From b6315cca0c6d07895cf888ebd6a2ba8e6b99d096 Mon Sep 17 00:00:00 2001 From: BB Date: Wed, 12 Feb 2025 14:45:46 -0500 Subject: [PATCH 01/22] Initial commit --- .gitignore | 25 +++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6f72f892 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env diff --git a/README.md b/README.md new file mode 100644 index 00000000..fefaa8d7 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# unbind-api +Unbind APIs From 4b10c183604c3a91b8eb43a9a6299995910d1b29 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Wed, 12 Feb 2025 15:30:59 -0500 Subject: [PATCH 02/22] oapi-codegen structure/new project --- .golangci-lint.yaml | 3 + README.md | 8 ++ api/generate.go | 2 + api/oapi-config.yaml | 7 + api/openapi.yaml | 52 +++++++ cmd/main.go | 27 ++++ go.mod | 33 +++++ go.sum | 187 +++++++++++++++++++++++++ internal/generated/openapi.gen.go | 220 ++++++++++++++++++++++++++++++ internal/server/projects.go | 19 +++ internal/server/server.go | 17 +++ tools/tools.go | 9 ++ 12 files changed, 584 insertions(+) create mode 100644 .golangci-lint.yaml create mode 100644 api/generate.go create mode 100644 api/oapi-config.yaml create mode 100644 api/openapi.yaml create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/generated/openapi.gen.go create mode 100644 internal/server/projects.go create mode 100644 internal/server/server.go create mode 100644 tools/tools.go diff --git a/.golangci-lint.yaml b/.golangci-lint.yaml new file mode 100644 index 00000000..6e64fed2 --- /dev/null +++ b/.golangci-lint.yaml @@ -0,0 +1,3 @@ +run: + skip-files: + - tools/tools.go \ No newline at end of file diff --git a/README.md b/README.md index fefaa8d7..7f158b05 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # unbind-api + Unbind APIs + +## Deps + +``` +# for the binary install +go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 +``` diff --git a/api/generate.go b/api/generate.go new file mode 100644 index 00000000..40ba1773 --- /dev/null +++ b/api/generate.go @@ -0,0 +1,2 @@ +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config=./oapi-config.yaml openapi.yaml +package api diff --git a/api/oapi-config.yaml b/api/oapi-config.yaml new file mode 100644 index 00000000..0d00b9c7 --- /dev/null +++ b/api/oapi-config.yaml @@ -0,0 +1,7 @@ +package: generated +generate: + - chi-server + - types +import-mapping: + # If your spec references external schemas in other packages, configure them here +output: ../internal/generated/openapi.gen.go diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 00000000..bd010787 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.4 +info: + title: Unbind API + version: 1.0.0 + +paths: + /health: + get: + operationId: HealthCheck + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + + /projects: + get: + operationId: ListProjects + parameters: + - name: page + in: query + required: false + schema: + type: integer + responses: + '200': + description: A list of projects. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + +components: + schemas: + Project: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..9efdcde7 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/unbindapp/unbind-api/internal/generated" + "github.com/unbindapp/unbind-api/internal/server" +) + +func main() { + // Implementation + srvImpl := &server.Server{} + + // New chi router + r := chi.NewRouter() + + // Register the routes from generated code + generated.HandlerFromMux(srvImpl, r) + + // Start the server + addr := ":8081" + fmt.Printf("Starting server on %s\n", addr) + log.Fatal(http.ListenAndServe(addr, r)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..43c04984 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module github.com/unbindapp/unbind-api + +go 1.23.6 + +require ( + github.com/go-chi/chi/v5 v5.2.1 + github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 + github.com/oapi-codegen/runtime v1.1.1 +) + +require github.com/ajg/form v1.5.1 // indirect + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/getkin/kin-openapi v0.127.0 // indirect + github.com/go-chi/render v1.0.3 + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..f0e098bb --- /dev/null +++ b/go.sum @@ -0,0 +1,187 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= +github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= +github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/generated/openapi.gen.go b/internal/generated/openapi.gen.go new file mode 100644 index 00000000..fe725005 --- /dev/null +++ b/internal/generated/openapi.gen.go @@ -0,0 +1,220 @@ +// Package generated provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +package generated + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/runtime" +) + +// Project defines model for Project. +type Project struct { + Id int64 `json:"id"` + Name string `json:"name"` +} + +// ListProjectsParams defines parameters for ListProjects. +type ListProjectsParams struct { + Page *int `form:"page,omitempty" json:"page,omitempty"` +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /health) + HealthCheck(w http.ResponseWriter, r *http.Request) + + // (GET /projects) + ListProjects(w http.ResponseWriter, r *http.Request, params ListProjectsParams) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct{} + +// (GET /health) +func (_ Unimplemented) HealthCheck(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (GET /projects) +func (_ Unimplemented) ListProjects(w http.ResponseWriter, r *http.Request, params ListProjectsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// HealthCheck operation middleware +func (siw *ServerInterfaceWrapper) HealthCheck(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.HealthCheck(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// ListProjects operation middleware +func (siw *ServerInterfaceWrapper) ListProjects(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params ListProjectsParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", r.URL.Query(), ¶ms.Page) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListProjects(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/health", wrapper.HealthCheck) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/projects", wrapper.ListProjects) + }) + + return r +} diff --git a/internal/server/projects.go b/internal/server/projects.go new file mode 100644 index 00000000..5c0b12d2 --- /dev/null +++ b/internal/server/projects.go @@ -0,0 +1,19 @@ +package server + +import ( + "net/http" + + "github.com/go-chi/render" + "github.com/unbindapp/unbind-api/internal/generated" +) + +// ListProjects handles GET /project +func (s *Server) ListProjects(w http.ResponseWriter, r *http.Request, params generated.ListProjectsParams) { + projects := []generated.Project{ + {Id: 1, Name: "ABC"}, + {Id: 2, Name: "DEF"}, + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, projects) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 00000000..3b476e5a --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,17 @@ +package server + +import ( + "net/http" +) + +// Server implements generated.ServerInterface +type Server struct { + // Add fields for DB connections, config, etc. as needed +} + +// HealthCheck is your /health endpoint +func (s *Server) HealthCheck(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) +} diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 00000000..f07678a0 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,9 @@ +//go:build tools +// +build tools + +package main + +//! TODO - go 1.24 fixes this so IDE won't whine - keep that in mind, when we update to 1.24 +import ( + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +) From 13b80b0c1a9554a80b6960f6ec8aa534ba01f8f7 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Sat, 15 Feb 2025 20:41:13 +0000 Subject: [PATCH 03/22] Initial API stuff, add teams API --- .data/kubernetes/.gitkeep | 0 .data/postgres/.gitkeep | 0 .data/zitadel/.gitkeep | 0 .devcontainer/devcontainer.json | 38 +++++ .devcontainer/docker-compose.yaml | 105 ++++++++++++ .gitignore | 4 + api/generate.go | 2 - api/oapi-config.yaml | 7 - api/openapi.yaml | 52 ------ cmd/main.go | 28 +++- config/config.go | 28 ++++ go.mod | 63 +++++--- go.sum | 258 +++++++++++++----------------- internal/generated/openapi.gen.go | 220 ------------------------- internal/kubeclient/kubeclient.go | 44 +++++ internal/kubeclient/teams.go | 55 +++++++ internal/log/logger.go | 56 +++++++ internal/server/projects.go | 19 --- internal/server/server.go | 20 ++- internal/server/teams.go | 28 ++++ scripts/init-zitadel.sh | 162 +++++++++++++++++++ tools/tools.go | 9 -- 22 files changed, 711 insertions(+), 487 deletions(-) create mode 100644 .data/kubernetes/.gitkeep create mode 100644 .data/postgres/.gitkeep create mode 100644 .data/zitadel/.gitkeep create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yaml delete mode 100644 api/generate.go delete mode 100644 api/oapi-config.yaml delete mode 100644 api/openapi.yaml create mode 100644 config/config.go delete mode 100644 internal/generated/openapi.gen.go create mode 100644 internal/kubeclient/kubeclient.go create mode 100644 internal/kubeclient/teams.go create mode 100644 internal/log/logger.go delete mode 100644 internal/server/projects.go create mode 100644 internal/server/teams.go create mode 100755 scripts/init-zitadel.sh delete mode 100644 tools/tools.go diff --git a/.data/kubernetes/.gitkeep b/.data/kubernetes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.data/postgres/.gitkeep b/.data/postgres/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.data/zitadel/.gitkeep b/.data/zitadel/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..538c3c90 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "Existing Docker Compose (Extend)", + "dockerComposeFile": [ + "./docker-compose.yaml" + ], + "service": "app", + "workspaceFolder": "/home/go/app", + "remoteUser": "go", + "settings": { + "terminal.integrated.inheritEnv": false, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.defaultProfile.windows": "zsh", + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/bin/zsh" + } + } + }, + "customizations": { + "vscode": { + "settings": {}, + "extensions": [ + "GitHub.copilot", + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml", + "streetsidesoftware.code-spell-checker", + "Gruntfuggly.todo-tree", + "golang.go", + "aaron-bond.better-comments", + "42Crunch.vscode-openapi" + ] + } + } +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml new file mode 100644 index 00000000..0948575a --- /dev/null +++ b/.devcontainer/docker-compose.yaml @@ -0,0 +1,105 @@ +version: '3.9' +services: + db: + container_name: unbind_postgres + image: postgres:15 + user: '1000:20' + ports: + - '127.0.0.1:53337:5432' + restart: unless-stopped + environment: + - POSTGRES_DB=zitadel + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - PGDATA=/var/lib/postgresql/data/dev + volumes: + - ../.data/postgres:/var/lib/postgresql/data:delegated + networks: + - app-network + healthcheck: + test: ['CMD-SHELL', 'pg_isready -d zitadel -U postgres'] + interval: 10s + timeout: 5s + retries: 5 + + zitadel: + image: ghcr.io/zitadel/zitadel:latest + restart: always + command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled + environment: + ZITADEL_EXTERNALPORT: 8082 + ZITADEL_DATABASE_POSTGRES_HOST: db + ZITADEL_DATABASE_POSTGRES_PORT: 5432 + ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel + ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel + ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel + ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable + ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres + ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres + ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable + ZITADEL_EXTERNALSECURE: 'false' + ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH: /data/zitadel/zitadel-admin-sa.json + ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa + ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin + ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE: 1 + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: 'false' + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin@unbind.app + ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: Password123! + ZITADEL_FIRSTINSTANCE_ORG_NAME: Unbind + volumes: + - ../.data/zitadel:/data/zitadel + networks: + - app-network + depends_on: + db: + condition: service_healthy + ports: + - '127.0.0.1:8082:8082' + + # zitadel-setup: + # image: appditto/go-vscode-dev:latest + # environment: + # - ZITADEL_MASTER_KEY=MasterkeyNeedsToHave32Characters + # depends_on: + # - zitadel + # volumes: + # - ../.data/zitadel:/data/zitadel + # - ./scripts/init-zitadel.sh:/scripts/init-zitadel.sh:ro + # entrypoint: ['/bin/sh', '/scripts/init-zitadel.sh'] + # networks: + # - app-network + # restart: 'no' + + app: + container_name: unbind_go_dev + image: appditto/go-vscode-dev:latest + security_opt: + - 'seccomp:unconfined' + environment: + - GOPRIVATE=github.com/unbindapp + - ZITADEL_MASTER_KEY=MasterkeyNeedsToHave32Characters + - PORT=8089 + - KUBECONFIG=./.data/kubernetes/kubeconfig.yaml + ports: + - '127.0.0.1:8089:8089' + volumes: + - ../.:/home/go/app + - ${HOME}/.gitconfig:/home/go/.gitconfig + - ${HOME}/.ssh:/home/go/.ssh + restart: on-failure + entrypoint: /bin/zsh + stdin_open: true + tty: true + user: go + depends_on: + - db + # - zitadel-setup + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + kind-data: \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f72f892..1256ae09 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ go.work.sum # env file .env + +# Devcontainer data +.data/*/** +!.data/*/.gitkeep \ No newline at end of file diff --git a/api/generate.go b/api/generate.go deleted file mode 100644 index 40ba1773..00000000 --- a/api/generate.go +++ /dev/null @@ -1,2 +0,0 @@ -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config=./oapi-config.yaml openapi.yaml -package api diff --git a/api/oapi-config.yaml b/api/oapi-config.yaml deleted file mode 100644 index 0d00b9c7..00000000 --- a/api/oapi-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -package: generated -generate: - - chi-server - - types -import-mapping: - # If your spec references external schemas in other packages, configure them here -output: ../internal/generated/openapi.gen.go diff --git a/api/openapi.yaml b/api/openapi.yaml deleted file mode 100644 index bd010787..00000000 --- a/api/openapi.yaml +++ /dev/null @@ -1,52 +0,0 @@ -openapi: 3.0.4 -info: - title: Unbind API - version: 1.0.0 - -paths: - /health: - get: - operationId: HealthCheck - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - status: - type: string - - /projects: - get: - operationId: ListProjects - parameters: - - name: page - in: query - required: false - schema: - type: integer - responses: - '200': - description: A list of projects. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Project' - -components: - schemas: - Project: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string diff --git a/cmd/main.go b/cmd/main.go index 9efdcde7..b3c35aa9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,23 +5,41 @@ import ( "log" "net/http" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" - "github.com/unbindapp/unbind-api/internal/generated" + "github.com/joho/godotenv" + "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/internal/kubeclient" "github.com/unbindapp/unbind-api/internal/server" ) func main() { + godotenv.Load() + + cfg := config.NewConfig() + // Initialize config + + // Create kubernetes client + kubeClient := kubeclient.NewKubeClient(cfg) + // Implementation - srvImpl := &server.Server{} + srvImpl := &server.Server{ + KubeClient: kubeClient, + } // New chi router r := chi.NewRouter() + api := humachi.New(r, huma.DefaultConfig("Unbind API", "1.0.0")) + + // Add routes + huma.Get(api, "/healthz", srvImpl.HealthCheck) - // Register the routes from generated code - generated.HandlerFromMux(srvImpl, r) + // ! TODO - auth stuff + huma.Get(api, "/teams", srvImpl.ListTeams) // Start the server - addr := ":8081" + addr := ":8089" fmt.Printf("Starting server on %s\n", addr) log.Fatal(http.ListenAndServe(addr, r)) } diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..ef9c0338 --- /dev/null +++ b/config/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "github.com/caarlos0/env/v11" + "github.com/unbindapp/unbind-api/internal/log" +) + +type Config struct { + // Postgres + PostgresHost string `env:"POSTGRES_HOST" envDefault:"localhost"` + PostgresPort int `env:"POSTGRES_PORT" envDefault:"5432"` + PostgresUser string `env:"POSTGRES_USER" envDefault:"postgres"` + PostgresPassword string `env:"POSTGRES_PASSWORD" envDefault:"postgres"` + // Zitadel + ZitadelClientID string `env:"ZITADEL_CLIENT_ID"` + ZitadelOidcKey string `env:"ZITADEL_OIDC_KEY"` + // Kubernetes config, optional - if in cluster it will use the in-cluster config + KubeConfig string `env:"KUBECONFIG"` +} + +// Parse environment variables into a Config struct +func NewConfig() *Config { + cfg := Config{} + if err := env.Parse(&cfg); err != nil { + log.Fatal("Error parsing environment", "err", err) + } + return &cfg +} diff --git a/go.mod b/go.mod index 43c04984..7c5f86d7 100644 --- a/go.mod +++ b/go.mod @@ -3,31 +3,48 @@ module github.com/unbindapp/unbind-api go 1.23.6 require ( + github.com/caarlos0/env/v11 v11.3.1 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/charmbracelet/log v0.4.0 + github.com/danielgtaylor/huma/v2 v2.28.0 github.com/go-chi/chi/v5 v5.2.1 - github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 - github.com/oapi-codegen/runtime v1.1.1 + github.com/joho/godotenv v1.5.1 + k8s.io/apimachinery v0.32.2 + k8s.io/client-go v0.32.2 ) -require github.com/ajg/form v1.5.1 // indirect - require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect - github.com/getkin/kin-openapi v0.127.0 // indirect - github.com/go-chi/render v1.0.3 - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/invopop/yaml v0.3.1 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect - github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.4.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.7.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index f0e098bb..82846ecc 100644 --- a/go.sum +++ b/go.sum @@ -1,187 +1,157 @@ -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/danielgtaylor/huma/v2 v2.28.0 h1:W+hIT52MigO73edJNJWXU896uC99xSBWpKoE2PRyybM= +github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wXiMIu94LV/cH2Ek= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= -github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= -github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= -github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= -github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= -github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= -github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= +k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= +k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= +k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/generated/openapi.gen.go b/internal/generated/openapi.gen.go deleted file mode 100644 index fe725005..00000000 --- a/internal/generated/openapi.gen.go +++ /dev/null @@ -1,220 +0,0 @@ -// Package generated provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. -package generated - -import ( - "fmt" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/oapi-codegen/runtime" -) - -// Project defines model for Project. -type Project struct { - Id int64 `json:"id"` - Name string `json:"name"` -} - -// ListProjectsParams defines parameters for ListProjects. -type ListProjectsParams struct { - Page *int `form:"page,omitempty" json:"page,omitempty"` -} - -// ServerInterface represents all server handlers. -type ServerInterface interface { - - // (GET /health) - HealthCheck(w http.ResponseWriter, r *http.Request) - - // (GET /projects) - ListProjects(w http.ResponseWriter, r *http.Request, params ListProjectsParams) -} - -// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. - -type Unimplemented struct{} - -// (GET /health) -func (_ Unimplemented) HealthCheck(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) -} - -// (GET /projects) -func (_ Unimplemented) ListProjects(w http.ResponseWriter, r *http.Request, params ListProjectsParams) { - w.WriteHeader(http.StatusNotImplemented) -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -type MiddlewareFunc func(http.Handler) http.Handler - -// HealthCheck operation middleware -func (siw *ServerInterfaceWrapper) HealthCheck(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.HealthCheck(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// ListProjects operation middleware -func (siw *ServerInterfaceWrapper) ListProjects(w http.ResponseWriter, r *http.Request) { - - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params ListProjectsParams - - // ------------- Optional query parameter "page" ------------- - - err = runtime.BindQueryParameter("form", true, false, "page", r.URL.Query(), ¶ms.Page) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.ListProjects(w, r, params) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -type UnescapedCookieParamError struct { - ParamName string - Err error -} - -func (e *UnescapedCookieParamError) Error() string { - return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) -} - -func (e *UnescapedCookieParamError) Unwrap() error { - return e.Err -} - -type UnmarshalingParamError struct { - ParamName string - Err error -} - -func (e *UnmarshalingParamError) Error() string { - return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) -} - -func (e *UnmarshalingParamError) Unwrap() error { - return e.Err -} - -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -type RequiredHeaderError struct { - ParamName string - Err error -} - -func (e *RequiredHeaderError) Error() string { - return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) -} - -func (e *RequiredHeaderError) Unwrap() error { - return e.Err -} - -type InvalidParamFormatError struct { - ParamName string - Err error -} - -func (e *InvalidParamFormatError) Error() string { - return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) -} - -func (e *InvalidParamFormatError) Unwrap() error { - return e.Err -} - -type TooManyValuesForParamError struct { - ParamName string - Count int -} - -func (e *TooManyValuesForParamError) Error() string { - return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) -} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{}) -} - -type ChiServerOptions struct { - BaseURL string - BaseRouter chi.Router - Middlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. -func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{ - BaseRouter: r, - }) -} - -func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { - return HandlerWithOptions(si, ChiServerOptions{ - BaseURL: baseURL, - BaseRouter: r, - }) -} - -// HandlerWithOptions creates http.Handler with additional options -func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { - r := options.BaseRouter - - if r == nil { - r = chi.NewRouter() - } - if options.ErrorHandlerFunc == nil { - options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/health", wrapper.HealthCheck) - }) - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/projects", wrapper.ListProjects) - }) - - return r -} diff --git a/internal/kubeclient/kubeclient.go b/internal/kubeclient/kubeclient.go new file mode 100644 index 00000000..20ecb11e --- /dev/null +++ b/internal/kubeclient/kubeclient.go @@ -0,0 +1,44 @@ +package kubeclient + +import ( + "log" + + "github.com/unbindapp/unbind-api/config" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type KubeClient struct { + config *config.Config + client *dynamic.DynamicClient +} + +func NewKubeClient(cfg *config.Config) *KubeClient { + var kubeConfig *rest.Config + var err error + + if cfg.KubeConfig != "" { + // Use provided kubeconfig if present + kubeConfig, err = clientcmd.BuildConfigFromFlags("", cfg.KubeConfig) + if err != nil { + log.Fatalf("Error building kubeconfig: %v", err) + } + } else { + // Fall back to in-cluster config + kubeConfig, err = rest.InClusterConfig() + if err != nil { + log.Fatalf("Error getting in-cluster config: %v", err) + } + } + + clientset, err := dynamic.NewForConfig(kubeConfig) + if err != nil { + log.Fatalf("Error creating clientset: %v", err) + } + + return &KubeClient{ + config: cfg, + client: clientset, + } +} diff --git a/internal/kubeclient/teams.go b/internal/kubeclient/teams.go new file mode 100644 index 00000000..7f70b98a --- /dev/null +++ b/internal/kubeclient/teams.go @@ -0,0 +1,55 @@ +package kubeclient + +// ? "Team" is synonymous with a Kubernetes namespace + +import ( + "context" + "fmt" + "sort" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// A team has a name and an underlying namespace +type UnbindTeam struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + CreatedAt time.Time `json:"created_at"` +} + +// GetUnbindTeams returns a slice of UnbindTeam structs +func (k *KubeClient) GetUnbindTeams() ([]UnbindTeam, error) { + namespaceRes := k.client.Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + namespaces, err := namespaceRes.List(context.Background(), metav1.ListOptions{ + LabelSelector: "unbind-team", + }) + if err != nil { + return nil, fmt.Errorf("error listing namespaces: %v", err) + } + + teams := make([]UnbindTeam, 0, len(namespaces.Items)) + for _, ns := range namespaces.Items { + teamValue := ns.GetLabels()["unbind-team"] + if teamValue != "" { + teams = append(teams, UnbindTeam{ + Name: teamValue, + Namespace: ns.GetName(), + CreatedAt: ns.GetCreationTimestamp().Time, + }) + } + } + + // Sort by data created by default + sort.Slice(teams, func(i, j int) bool { + return teams[i].CreatedAt.After(teams[j].CreatedAt) + }) + + return teams, nil +} diff --git a/internal/log/logger.go b/internal/log/logger.go new file mode 100644 index 00000000..cb9a0f37 --- /dev/null +++ b/internal/log/logger.go @@ -0,0 +1,56 @@ +package log + +import ( + "os" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" +) + +var logger *log.Logger + +func getLogger() *log.Logger { + if logger == nil { + styles := log.DefaultStyles() + styles.Levels[log.FatalLevel] = lipgloss.NewStyle().SetString("☠️🟥☠️") + styles.Levels[log.ErrorLevel] = lipgloss.NewStyle().SetString("🟥") + styles.Levels[log.WarnLevel] = lipgloss.NewStyle().SetString("🟨") + styles.Levels[log.InfoLevel] = lipgloss.NewStyle().SetString("🟦") + logger = log.New(os.Stderr) + logger.SetStyles(styles) + /* logger.SetReportTimestamp(true) */ + } + return logger +} + +func Info(msg interface{}, keyvals ...interface{}) { + getLogger().Info(msg, keyvals...) +} + +func Infof(format string, args ...any) { + getLogger().Infof(format, args...) +} + +func Error(msg interface{}, keyvals ...interface{}) { + getLogger().Error(msg, keyvals...) +} + +func Errorf(format string, args ...any) { + getLogger().Errorf(format, args...) +} + +func Warn(msg interface{}, keyvals ...interface{}) { + getLogger().Warn(msg, keyvals...) +} + +func Warnf(format string, args ...any) { + getLogger().Warnf(format, args...) +} + +func Fatal(msg interface{}, keyvals ...interface{}) { + getLogger().Fatal(msg, keyvals...) +} + +func Fatalf(format string, args ...any) { + getLogger().Fatalf(format, args...) +} diff --git a/internal/server/projects.go b/internal/server/projects.go deleted file mode 100644 index 5c0b12d2..00000000 --- a/internal/server/projects.go +++ /dev/null @@ -1,19 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/go-chi/render" - "github.com/unbindapp/unbind-api/internal/generated" -) - -// ListProjects handles GET /project -func (s *Server) ListProjects(w http.ResponseWriter, r *http.Request, params generated.ListProjectsParams) { - projects := []generated.Project{ - {Id: 1, Name: "ABC"}, - {Id: 2, Name: "DEF"}, - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, projects) -} diff --git a/internal/server/server.go b/internal/server/server.go index 3b476e5a..72557a47 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,17 +1,25 @@ package server import ( - "net/http" + "context" + + "github.com/unbindapp/unbind-api/internal/kubeclient" ) // Server implements generated.ServerInterface type Server struct { - // Add fields for DB connections, config, etc. as needed + KubeClient *kubeclient.KubeClient } // HealthCheck is your /health endpoint -func (s *Server) HealthCheck(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) +type HealthResponse struct { + Body struct { + Status string `json:"status"` + } +} + +func (s *Server) HealthCheck(ctx context.Context, _ *struct{}) (*HealthResponse, error) { + healthResponse := &HealthResponse{} + healthResponse.Body.Status = "ok" + return healthResponse, nil } diff --git a/internal/server/teams.go b/internal/server/teams.go new file mode 100644 index 00000000..f10bb0e3 --- /dev/null +++ b/internal/server/teams.go @@ -0,0 +1,28 @@ +package server + +import ( + "context" + + "github.com/danielgtaylor/huma/v2" + "github.com/unbindapp/unbind-api/internal/kubeclient" + "github.com/unbindapp/unbind-api/internal/log" +) + +type TeamResponse struct { + Body struct { + Teams []kubeclient.UnbindTeam `json:"teams"` + } +} + +// ListTeams handles GET /teams +func (s *Server) ListTeams(ctx context.Context, _ *struct{}) (*TeamResponse, error) { + teams, err := s.KubeClient.GetUnbindTeams() + if err != nil { + log.Error("Error getting teams", "err", err) + return nil, huma.Error500InternalServerError("Unable to retrieve teams") + } + + resp := &TeamResponse{} + resp.Body.Teams = teams + return resp, nil +} diff --git a/scripts/init-zitadel.sh b/scripts/init-zitadel.sh new file mode 100755 index 00000000..35e72aec --- /dev/null +++ b/scripts/init-zitadel.sh @@ -0,0 +1,162 @@ +#!/bin/bash +set -e + +# Function to base64url encode +base64url() { + base64 | tr '+/' '-_' | tr -d '=' +} + +# Wait for Zitadel to be ready +wait_for_zitadel() { + echo "Waiting for Zitadel to be ready..." + until curl -s http://zitadel:8080/healthz | grep -q "SERVING"; do + echo "Waiting for Zitadel..." + sleep 2 + done + echo "Zitadel is ready!" +} + +# Function to get access token using service account +get_access_token() { + local SA_KEY_FILE="./.data/zitadel/zitadel-admin-sa.json" + if [ ! -f "$SA_KEY_FILE" ]; then + echo "Service account key file not found at $SA_KEY_FILE" + exit 1 + fi + + echo "Reading service account key file..." + cat "$SA_KEY_FILE" + + # Extract necessary fields from the JSON key file + local KEY_ID=$(jq -r .keyId "$SA_KEY_FILE") + if [ -z "$KEY_ID" ]; then + echo "Failed to extract keyId from service account file" + exit 1 + fi + echo "Found key ID: $KEY_ID" + + local KEY=$(jq -r .key "$SA_KEY_FILE") + if [ -z "$KEY" ]; then + echo "Failed to extract key from service account file" + exit 1 + fi + echo "Found private key" + + # Create JWT token + local NOW=$(date +%s) + local EXP=$((NOW + 3600)) + + local HEADER='{"alg":"RS256","kid":"'$KEY_ID'"}' + local PAYLOAD='{"aud":["zitadel"],"exp":'$EXP',"iat":'$NOW',"iss":"zitadel-admin-sa","sub":"zitadel-admin-sa","scope":["openid","profile","email"]}' + + echo "Created header: $HEADER" + echo "Created payload: $PAYLOAD" + + # Base64URL encode header and payload + local B64_HEADER=$(echo -n "$HEADER" | base64url) + local B64_PAYLOAD=$(echo -n "$PAYLOAD" | base64url) + + # Create signature + echo "Signing JWT..." + echo "$KEY" > /tmp/private.key + local SIGNATURE=$(echo -n "$B64_HEADER.$B64_PAYLOAD" | openssl dgst -sha256 -sign /tmp/private.key | base64url) + rm /tmp/private.key + + local JWT="$B64_HEADER.$B64_PAYLOAD.$SIGNATURE" + echo "Created JWT: $JWT" + + # Get the access token + echo "Requesting access token from Zitadel..." + local RESPONSE=$(curl -v -X POST "http://zitadel:8080/oauth/v2/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ + -d "assertion=$JWT") + + echo "Token response: $RESPONSE" + local ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token) + + if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + echo "Failed to get access token" + exit 1 + fi + + echo "$ACCESS_TOKEN" +} + +# Create project and OIDC configuration +setup_project() { + local ACCESS_TOKEN=$1 + + # Create project + echo "Creating Unbind API project..." + local PROJECT_RESPONSE=$(curl -s -X POST "http://zitadel:8080/oauth/v2/projects" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Unbind API", + "projectRoleAssertion": true, + "projectRoleCheck": true + }') + + echo "Project creation response: $PROJECT_RESPONSE" + local PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r .id) + + if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then + echo "Failed to create project" + exit 1 + fi + + # Create OIDC application + echo "Creating OIDC application..." + local APP_RESPONSE=$(curl -s -X POST "http://zitadel:8080/oauth/v2/projects/$PROJECT_ID/apps/oidc" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Unbind Local Dev", + "redirectUris": ["http://localhost:8089/auth/callback"], + "responseTypes": ["CODE"], + "grantTypes": ["AUTHORIZATION_CODE", "REFRESH_TOKEN"], + "appType": "USER_AGENT", + "authMethodType": "NONE", + "version": "V2" + }') + + echo "App creation response: $APP_RESPONSE" + + # Save OIDC configuration + local CLIENT_ID=$(echo "$APP_RESPONSE" | jq -r .clientId) + local CLIENT_SECRET=$(echo "$APP_RESPONSE" | jq -r .clientSecret) + + if [ -z "$CLIENT_ID" ] || [ "$CLIENT_ID" = "null" ]; then + echo "Failed to create OIDC application" + exit 1 + fi + + echo "Saving OIDC configuration..." + echo '{ + "issuer": "http://localhost:8082", + "clientId": "'$CLIENT_ID'", + "clientSecret": "'$CLIENT_SECRET'", + "redirectUri": "http://localhost:8089/auth/callback" + }' > ./.data/zitadel/oidc.json + + echo "OIDC configuration saved to ./.data/zitadel/oidc.json" +} + +main() { + wait_for_zitadel + + echo "Getting access token..." + ACCESS_TOKEN=$(get_access_token) + + if [ -z "$ACCESS_TOKEN" ]; then + echo "Failed to get access token" + exit 1 + fi + + setup_project "$ACCESS_TOKEN" + + echo "Zitadel initialization completed successfully!" +} + +main \ No newline at end of file diff --git a/tools/tools.go b/tools/tools.go deleted file mode 100644 index f07678a0..00000000 --- a/tools/tools.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build tools -// +build tools - -package main - -//! TODO - go 1.24 fixes this so IDE won't whine - keep that in mind, when we update to 1.24 -import ( - _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" -) From 6df8bb63ce03dc0ca05e9bbef45dcbd30eaf33f9 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 17 Feb 2025 20:59:14 +0000 Subject: [PATCH 04/22] Various API structure and swap to dex --- .data/zitadel/.gitkeep | 0 .devcontainer/dex.yaml | 24 + .devcontainer/docker-compose.yaml | 69 +- .devcontainer/init-db.sql | 4 + .mockery.yaml | 17 + config/config.go | 1 + ent/client.go | 367 +++++++++ ent/ent.go | 608 +++++++++++++++ ent/enttest/enttest.go | 84 +++ ent/generate.go | 3 + ent/hook/hook.go | 199 +++++ ent/migrate/migrate.go | 64 ++ ent/migrate/schema.go | 33 + ent/mutation.go | 577 ++++++++++++++ ent/predicate/predicate.go | 10 + ent/runtime.go | 38 + ent/runtime/runtime.go | 10 + ent/schema/mixin/pk.go | 23 + ent/schema/mixin/time.go | 26 + ent/schema/user.go | 36 + ent/tx.go | 236 ++++++ ent/user.go | 152 ++++ ent/user/user.go | 93 +++ ent/user/where.go | 371 +++++++++ ent/user_create.go | 711 ++++++++++++++++++ ent/user_delete.go | 88 +++ ent/user_query.go | 551 ++++++++++++++ ent/user_update.go | 330 ++++++++ go.mod | 52 +- go.sum | 124 ++- internal/auth/middleware.go | 94 +++ internal/database/config.go | 77 ++ internal/database/config_test.go | 33 + internal/database/entclient.go | 20 + internal/database/entclient_test.go | 15 + internal/database/repository/repository.go | 64 ++ .../database/repository/repository_iface.go | 22 + .../database/repository/repository_test.go | 131 ++++ mocks/repository/repository_mock.go | 84 +++ mocks/repository/tx/tx_mock.go | 172 +++++ scripts/clear-local-data.sh | 5 + scripts/init-zitadel.sh | 162 ---- 42 files changed, 5546 insertions(+), 234 deletions(-) delete mode 100644 .data/zitadel/.gitkeep create mode 100644 .devcontainer/dex.yaml create mode 100644 .devcontainer/init-db.sql create mode 100644 .mockery.yaml create mode 100644 ent/client.go create mode 100644 ent/ent.go create mode 100644 ent/enttest/enttest.go create mode 100644 ent/generate.go create mode 100644 ent/hook/hook.go create mode 100644 ent/migrate/migrate.go create mode 100644 ent/migrate/schema.go create mode 100644 ent/mutation.go create mode 100644 ent/predicate/predicate.go create mode 100644 ent/runtime.go create mode 100644 ent/runtime/runtime.go create mode 100644 ent/schema/mixin/pk.go create mode 100644 ent/schema/mixin/time.go create mode 100644 ent/schema/user.go create mode 100644 ent/tx.go create mode 100644 ent/user.go create mode 100644 ent/user/user.go create mode 100644 ent/user/where.go create mode 100644 ent/user_create.go create mode 100644 ent/user_delete.go create mode 100644 ent/user_query.go create mode 100644 ent/user_update.go create mode 100644 internal/auth/middleware.go create mode 100644 internal/database/config.go create mode 100644 internal/database/config_test.go create mode 100644 internal/database/entclient.go create mode 100644 internal/database/entclient_test.go create mode 100644 internal/database/repository/repository.go create mode 100644 internal/database/repository/repository_iface.go create mode 100644 internal/database/repository/repository_test.go create mode 100644 mocks/repository/repository_mock.go create mode 100644 mocks/repository/tx/tx_mock.go create mode 100755 scripts/clear-local-data.sh delete mode 100755 scripts/init-zitadel.sh diff --git a/.data/zitadel/.gitkeep b/.data/zitadel/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.devcontainer/dex.yaml b/.devcontainer/dex.yaml new file mode 100644 index 00000000..60464181 --- /dev/null +++ b/.devcontainer/dex.yaml @@ -0,0 +1,24 @@ +issuer: http://127.0.0.1:5556 +storage: + type: memory +web: + http: 0.0.0.0:5556 + +enablePasswordDB: true +staticPasswords: + - email: "admin@example.com" + # Password is "password" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "12345678-1234-5678-1234-567812345678" + +oauth2: + skipApprovalScreen: true + +staticClients: + - id: my-go-app + secret: supersecret + name: "Local Dev Example" + redirectURIs: + - http://localhost:8089/callback + - http://127.0.0.1:8089/callback diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index 0948575a..5687cfc3 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -8,7 +8,6 @@ services: - '127.0.0.1:53337:5432' restart: unless-stopped environment: - - POSTGRES_DB=zitadel - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - PGDATA=/var/lib/postgresql/data/dev @@ -16,59 +15,18 @@ services: - ../.data/postgres:/var/lib/postgresql/data:delegated networks: - app-network - healthcheck: - test: ['CMD-SHELL', 'pg_isready -d zitadel -U postgres'] - interval: 10s - timeout: 5s - retries: 5 - zitadel: - image: ghcr.io/zitadel/zitadel:latest - restart: always - command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled - environment: - ZITADEL_EXTERNALPORT: 8082 - ZITADEL_DATABASE_POSTGRES_HOST: db - ZITADEL_DATABASE_POSTGRES_PORT: 5432 - ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel - ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - ZITADEL_EXTERNALSECURE: 'false' - ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH: /data/zitadel/zitadel-admin-sa.json - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE: 1 - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: 'false' - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin@unbind.app - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: Password123! - ZITADEL_FIRSTINSTANCE_ORG_NAME: Unbind - volumes: - - ../.data/zitadel:/data/zitadel + dex: + container_name: unbind_dex + image: ghcr.io/dexidp/dex:v2.41.1 + restart: unless-stopped networks: - app-network - depends_on: - db: - condition: service_healthy + volumes: + - ./dex.yaml:/etc/dex/config.yaml:ro ports: - - '127.0.0.1:8082:8082' - - # zitadel-setup: - # image: appditto/go-vscode-dev:latest - # environment: - # - ZITADEL_MASTER_KEY=MasterkeyNeedsToHave32Characters - # depends_on: - # - zitadel - # volumes: - # - ../.data/zitadel:/data/zitadel - # - ./scripts/init-zitadel.sh:/scripts/init-zitadel.sh:ro - # entrypoint: ['/bin/sh', '/scripts/init-zitadel.sh'] - # networks: - # - app-network - # restart: 'no' + - '5556:5556' # Expose Dex on localhost:5556 + command: ['dex', 'serve', '/etc/dex/config.yaml'] app: container_name: unbind_go_dev @@ -77,9 +35,13 @@ services: - 'seccomp:unconfined' environment: - GOPRIVATE=github.com/unbindapp - - ZITADEL_MASTER_KEY=MasterkeyNeedsToHave32Characters - PORT=8089 - KUBECONFIG=./.data/kubernetes/kubeconfig.yaml + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=unbind ports: - '127.0.0.1:8089:8089' volumes: @@ -93,13 +55,10 @@ services: user: go depends_on: - db - # - zitadel-setup + - dex networks: - app-network networks: app-network: driver: bridge - -volumes: - kind-data: \ No newline at end of file diff --git a/.devcontainer/init-db.sql b/.devcontainer/init-db.sql new file mode 100644 index 00000000..716affc4 --- /dev/null +++ b/.devcontainer/init-db.sql @@ -0,0 +1,4 @@ +CREATE DATABASE authentik; +CREATE DATABASE unbind; +GRANT ALL PRIVILEGES ON DATABASE authentik TO postgres; +GRANT ALL PRIVILEGES ON DATABASE unbind TO postgres; \ No newline at end of file diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 00000000..3328257c --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,17 @@ +with-expecter: true +packages: + github.com/unbindapp/unbind-api/internal/database/repository: + config: + dir: ./mocks/repository + interfaces: + RepositoryInterface: + config: + filename: repository_mock.go + mockName: RepositoryMock + outpkg: mocks_repository + TxInterface: + config: + dir: ./mocks/repository/tx + filename: tx_mock.go + mockName: TxMock + outpkg: mocks_repository_tx diff --git a/config/config.go b/config/config.go index ef9c0338..d3b7c600 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ type Config struct { PostgresPort int `env:"POSTGRES_PORT" envDefault:"5432"` PostgresUser string `env:"POSTGRES_USER" envDefault:"postgres"` PostgresPassword string `env:"POSTGRES_PASSWORD" envDefault:"postgres"` + PostgresDB string `env:"POSTGRES_DB" envDefault:"unbind"` // Zitadel ZitadelClientID string `env:"ZITADEL_CLIENT_ID"` ZitadelOidcKey string `env:"ZITADEL_OIDC_KEY"` diff --git a/ent/client.go b/ent/client.go new file mode 100644 index 00000000..e042ca59 --- /dev/null +++ b/ent/client.go @@ -0,0 +1,367 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "log" + "reflect" + + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/migrate" + + "entgo.io/ent" + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "github.com/unbindapp/unbind-api/ent/user" + + stdsql "database/sql" +) + +// Client is the client that holds all ent builders. +type Client struct { + config + // Schema is the client for creating, migrating and dropping schema. + Schema *migrate.Schema + // User is the client for interacting with the User builders. + User *UserClient +} + +// NewClient creates a new client configured with the given options. +func NewClient(opts ...Option) *Client { + client := &Client{config: newConfig(opts...)} + client.init() + return client +} + +func (c *Client) init() { + c.Schema = migrate.NewSchema(c.driver) + c.User = NewUserClient(c.config) +} + +type ( + // config is the configuration for the client and its builder. + config struct { + // driver used for executing database requests. + driver dialect.Driver + // debug enable a debug logging. + debug bool + // log used for logging on debug mode. + log func(...any) + // hooks to execute on mutations. + hooks *hooks + // interceptors to execute on queries. + inters *inters + } + // Option function to configure the client. + Option func(*config) +) + +// newConfig creates a new config for the client. +func newConfig(opts ...Option) config { + cfg := config{log: log.Println, hooks: &hooks{}, inters: &inters{}} + cfg.options(opts...) + return cfg +} + +// options applies the options on the config object. +func (c *config) options(opts ...Option) { + for _, opt := range opts { + opt(c) + } + if c.debug { + c.driver = dialect.Debug(c.driver, c.log) + } +} + +// Debug enables debug logging on the ent.Driver. +func Debug() Option { + return func(c *config) { + c.debug = true + } +} + +// Log sets the logging function for debug mode. +func Log(fn func(...any)) Option { + return func(c *config) { + c.log = fn + } +} + +// Driver configures the client driver. +func Driver(driver dialect.Driver) Option { + return func(c *config) { + c.driver = driver + } +} + +// Open opens a database/sql.DB specified by the driver name and +// the data source name, and returns a new client attached to it. +// Optional parameters can be added for configuring the client. +func Open(driverName, dataSourceName string, options ...Option) (*Client, error) { + switch driverName { + case dialect.MySQL, dialect.Postgres, dialect.SQLite: + drv, err := sql.Open(driverName, dataSourceName) + if err != nil { + return nil, err + } + return NewClient(append(options, Driver(drv))...), nil + default: + return nil, fmt.Errorf("unsupported driver: %q", driverName) + } +} + +// ErrTxStarted is returned when trying to start a new transaction from a transactional client. +var ErrTxStarted = errors.New("ent: cannot start a transaction within a transaction") + +// Tx returns a new transactional client. The provided context +// is used until the transaction is committed or rolled back. +func (c *Client) Tx(ctx context.Context) (*Tx, error) { + if _, ok := c.driver.(*txDriver); ok { + return nil, ErrTxStarted + } + tx, err := newTx(ctx, c.driver) + if err != nil { + return nil, fmt.Errorf("ent: starting a transaction: %w", err) + } + cfg := c.config + cfg.driver = tx + return &Tx{ + ctx: ctx, + config: cfg, + User: NewUserClient(cfg), + }, nil +} + +// BeginTx returns a transactional client with specified options. +func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { + if _, ok := c.driver.(*txDriver); ok { + return nil, errors.New("ent: cannot start a transaction within a transaction") + } + tx, err := c.driver.(interface { + BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error) + }).BeginTx(ctx, opts) + if err != nil { + return nil, fmt.Errorf("ent: starting a transaction: %w", err) + } + cfg := c.config + cfg.driver = &txDriver{tx: tx, drv: c.driver} + return &Tx{ + ctx: ctx, + config: cfg, + User: NewUserClient(cfg), + }, nil +} + +// Debug returns a new debug-client. It's used to get verbose logging on specific operations. +// +// client.Debug(). +// User. +// Query(). +// Count(ctx) +func (c *Client) Debug() *Client { + if c.debug { + return c + } + cfg := c.config + cfg.driver = dialect.Debug(c.driver, c.log) + client := &Client{config: cfg} + client.init() + return client +} + +// Close closes the database connection and prevents new queries from starting. +func (c *Client) Close() error { + return c.driver.Close() +} + +// Use adds the mutation hooks to all the entity clients. +// In order to add hooks to a specific client, call: `client.Node.Use(...)`. +func (c *Client) Use(hooks ...Hook) { + c.User.Use(hooks...) +} + +// Intercept adds the query interceptors to all the entity clients. +// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. +func (c *Client) Intercept(interceptors ...Interceptor) { + c.User.Intercept(interceptors...) +} + +// Mutate implements the ent.Mutator interface. +func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { + switch m := m.(type) { + case *UserMutation: + return c.User.mutate(ctx, m) + default: + return nil, fmt.Errorf("ent: unknown mutation type %T", m) + } +} + +// UserClient is a client for the User schema. +type UserClient struct { + config +} + +// NewUserClient returns a client for the User from the given config. +func NewUserClient(c config) *UserClient { + return &UserClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `user.Hooks(f(g(h())))`. +func (c *UserClient) Use(hooks ...Hook) { + c.hooks.User = append(c.hooks.User, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `user.Intercept(f(g(h())))`. +func (c *UserClient) Intercept(interceptors ...Interceptor) { + c.inters.User = append(c.inters.User, interceptors...) +} + +// Create returns a builder for creating a User entity. +func (c *UserClient) Create() *UserCreate { + mutation := newUserMutation(c.config, OpCreate) + return &UserCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of User entities. +func (c *UserClient) CreateBulk(builders ...*UserCreate) *UserCreateBulk { + return &UserCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *UserClient) MapCreateBulk(slice any, setFunc func(*UserCreate, int)) *UserCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &UserCreateBulk{err: fmt.Errorf("calling to UserClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*UserCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &UserCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for User. +func (c *UserClient) Update() *UserUpdate { + mutation := newUserMutation(c.config, OpUpdate) + return &UserUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *UserClient) UpdateOne(u *User) *UserUpdateOne { + mutation := newUserMutation(c.config, OpUpdateOne, withUser(u)) + return &UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *UserClient) UpdateOneID(id uuid.UUID) *UserUpdateOne { + mutation := newUserMutation(c.config, OpUpdateOne, withUserID(id)) + return &UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for User. +func (c *UserClient) Delete() *UserDelete { + mutation := newUserMutation(c.config, OpDelete) + return &UserDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *UserClient) DeleteOne(u *User) *UserDeleteOne { + return c.DeleteOneID(u.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *UserClient) DeleteOneID(id uuid.UUID) *UserDeleteOne { + builder := c.Delete().Where(user.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &UserDeleteOne{builder} +} + +// Query returns a query builder for User. +func (c *UserClient) Query() *UserQuery { + return &UserQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeUser}, + inters: c.Interceptors(), + } +} + +// Get returns a User entity by its id. +func (c *UserClient) Get(ctx context.Context, id uuid.UUID) (*User, error) { + return c.Query().Where(user.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *UserClient) GetX(ctx context.Context, id uuid.UUID) *User { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *UserClient) Hooks() []Hook { + return c.hooks.User +} + +// Interceptors returns the client interceptors. +func (c *UserClient) Interceptors() []Interceptor { + return c.inters.User +} + +func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&UserCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&UserUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&UserDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown User mutation op: %q", m.Op()) + } +} + +// hooks and interceptors per client, for fast access. +type ( + hooks struct { + User []ent.Hook + } + inters struct { + User []ent.Interceptor + } +) + +// ExecContext allows calling the underlying ExecContext method of the driver if it is supported by it. +// See, database/sql#DB.ExecContext for more information. +func (c *config) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error) { + ex, ok := c.driver.(interface { + ExecContext(context.Context, string, ...any) (stdsql.Result, error) + }) + if !ok { + return nil, fmt.Errorf("Driver.ExecContext is not supported") + } + return ex.ExecContext(ctx, query, args...) +} + +// QueryContext allows calling the underlying QueryContext method of the driver if it is supported by it. +// See, database/sql#DB.QueryContext for more information. +func (c *config) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error) { + q, ok := c.driver.(interface { + QueryContext(context.Context, string, ...any) (*stdsql.Rows, error) + }) + if !ok { + return nil, fmt.Errorf("Driver.QueryContext is not supported") + } + return q.QueryContext(ctx, query, args...) +} diff --git a/ent/ent.go b/ent/ent.go new file mode 100644 index 00000000..7a46d066 --- /dev/null +++ b/ent/ent.go @@ -0,0 +1,608 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/unbindapp/unbind-api/ent/user" +) + +// ent aliases to avoid import conflicts in user's code. +type ( + Op = ent.Op + Hook = ent.Hook + Value = ent.Value + Query = ent.Query + QueryContext = ent.QueryContext + Querier = ent.Querier + QuerierFunc = ent.QuerierFunc + Interceptor = ent.Interceptor + InterceptFunc = ent.InterceptFunc + Traverser = ent.Traverser + TraverseFunc = ent.TraverseFunc + Policy = ent.Policy + Mutator = ent.Mutator + Mutation = ent.Mutation + MutateFunc = ent.MutateFunc +) + +type clientCtxKey struct{} + +// FromContext returns a Client stored inside a context, or nil if there isn't one. +func FromContext(ctx context.Context) *Client { + c, _ := ctx.Value(clientCtxKey{}).(*Client) + return c +} + +// NewContext returns a new context with the given Client attached. +func NewContext(parent context.Context, c *Client) context.Context { + return context.WithValue(parent, clientCtxKey{}, c) +} + +type txCtxKey struct{} + +// TxFromContext returns a Tx stored inside a context, or nil if there isn't one. +func TxFromContext(ctx context.Context) *Tx { + tx, _ := ctx.Value(txCtxKey{}).(*Tx) + return tx +} + +// NewTxContext returns a new context with the given Tx attached. +func NewTxContext(parent context.Context, tx *Tx) context.Context { + return context.WithValue(parent, txCtxKey{}, tx) +} + +// OrderFunc applies an ordering on the sql selector. +// Deprecated: Use Asc/Desc functions or the package builders instead. +type OrderFunc func(*sql.Selector) + +var ( + initCheck sync.Once + columnCheck sql.ColumnCheck +) + +// checkColumn checks if the column exists in the given table. +func checkColumn(table, column string) error { + initCheck.Do(func() { + columnCheck = sql.NewColumnCheck(map[string]func(string) bool{ + user.Table: user.ValidColumn, + }) + }) + return columnCheck(table, column) +} + +// Asc applies the given fields in ASC order. +func Asc(fields ...string) func(*sql.Selector) { + return func(s *sql.Selector) { + for _, f := range fields { + if err := checkColumn(s.TableName(), f); err != nil { + s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) + } + s.OrderBy(sql.Asc(s.C(f))) + } + } +} + +// Desc applies the given fields in DESC order. +func Desc(fields ...string) func(*sql.Selector) { + return func(s *sql.Selector) { + for _, f := range fields { + if err := checkColumn(s.TableName(), f); err != nil { + s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) + } + s.OrderBy(sql.Desc(s.C(f))) + } + } +} + +// AggregateFunc applies an aggregation step on the group-by traversal/selector. +type AggregateFunc func(*sql.Selector) string + +// As is a pseudo aggregation function for renaming another other functions with custom names. For example: +// +// GroupBy(field1, field2). +// Aggregate(ent.As(ent.Sum(field1), "sum_field1"), (ent.As(ent.Sum(field2), "sum_field2")). +// Scan(ctx, &v) +func As(fn AggregateFunc, end string) AggregateFunc { + return func(s *sql.Selector) string { + return sql.As(fn(s), end) + } +} + +// Count applies the "count" aggregation function on each group. +func Count() AggregateFunc { + return func(s *sql.Selector) string { + return sql.Count("*") + } +} + +// Max applies the "max" aggregation function on the given field of each group. +func Max(field string) AggregateFunc { + return func(s *sql.Selector) string { + if err := checkColumn(s.TableName(), field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Max(s.C(field)) + } +} + +// Mean applies the "mean" aggregation function on the given field of each group. +func Mean(field string) AggregateFunc { + return func(s *sql.Selector) string { + if err := checkColumn(s.TableName(), field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Avg(s.C(field)) + } +} + +// Min applies the "min" aggregation function on the given field of each group. +func Min(field string) AggregateFunc { + return func(s *sql.Selector) string { + if err := checkColumn(s.TableName(), field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Min(s.C(field)) + } +} + +// Sum applies the "sum" aggregation function on the given field of each group. +func Sum(field string) AggregateFunc { + return func(s *sql.Selector) string { + if err := checkColumn(s.TableName(), field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Sum(s.C(field)) + } +} + +// ValidationError returns when validating a field or edge fails. +type ValidationError struct { + Name string // Field or edge name. + err error +} + +// Error implements the error interface. +func (e *ValidationError) Error() string { + return e.err.Error() +} + +// Unwrap implements the errors.Wrapper interface. +func (e *ValidationError) Unwrap() error { + return e.err +} + +// IsValidationError returns a boolean indicating whether the error is a validation error. +func IsValidationError(err error) bool { + if err == nil { + return false + } + var e *ValidationError + return errors.As(err, &e) +} + +// NotFoundError returns when trying to fetch a specific entity and it was not found in the database. +type NotFoundError struct { + label string +} + +// Error implements the error interface. +func (e *NotFoundError) Error() string { + return "ent: " + e.label + " not found" +} + +// IsNotFound returns a boolean indicating whether the error is a not found error. +func IsNotFound(err error) bool { + if err == nil { + return false + } + var e *NotFoundError + return errors.As(err, &e) +} + +// MaskNotFound masks not found error. +func MaskNotFound(err error) error { + if IsNotFound(err) { + return nil + } + return err +} + +// NotSingularError returns when trying to fetch a singular entity and more then one was found in the database. +type NotSingularError struct { + label string +} + +// Error implements the error interface. +func (e *NotSingularError) Error() string { + return "ent: " + e.label + " not singular" +} + +// IsNotSingular returns a boolean indicating whether the error is a not singular error. +func IsNotSingular(err error) bool { + if err == nil { + return false + } + var e *NotSingularError + return errors.As(err, &e) +} + +// NotLoadedError returns when trying to get a node that was not loaded by the query. +type NotLoadedError struct { + edge string +} + +// Error implements the error interface. +func (e *NotLoadedError) Error() string { + return "ent: " + e.edge + " edge was not loaded" +} + +// IsNotLoaded returns a boolean indicating whether the error is a not loaded error. +func IsNotLoaded(err error) bool { + if err == nil { + return false + } + var e *NotLoadedError + return errors.As(err, &e) +} + +// ConstraintError returns when trying to create/update one or more entities and +// one or more of their constraints failed. For example, violation of edge or +// field uniqueness. +type ConstraintError struct { + msg string + wrap error +} + +// Error implements the error interface. +func (e ConstraintError) Error() string { + return "ent: constraint failed: " + e.msg +} + +// Unwrap implements the errors.Wrapper interface. +func (e *ConstraintError) Unwrap() error { + return e.wrap +} + +// IsConstraintError returns a boolean indicating whether the error is a constraint failure. +func IsConstraintError(err error) bool { + if err == nil { + return false + } + var e *ConstraintError + return errors.As(err, &e) +} + +// selector embedded by the different Select/GroupBy builders. +type selector struct { + label string + flds *[]string + fns []AggregateFunc + scan func(context.Context, any) error +} + +// ScanX is like Scan, but panics if an error occurs. +func (s *selector) ScanX(ctx context.Context, v any) { + if err := s.scan(ctx, v); err != nil { + panic(err) + } +} + +// Strings returns list of strings from a selector. It is only allowed when selecting one field. +func (s *selector) Strings(ctx context.Context) ([]string, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Strings is not achievable when selecting more than 1 field") + } + var v []string + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// StringsX is like Strings, but panics if an error occurs. +func (s *selector) StringsX(ctx context.Context) []string { + v, err := s.Strings(ctx) + if err != nil { + panic(err) + } + return v +} + +// String returns a single string from a selector. It is only allowed when selecting one field. +func (s *selector) String(ctx context.Context) (_ string, err error) { + var v []string + if v, err = s.Strings(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Strings returned %d results when one was expected", len(v)) + } + return +} + +// StringX is like String, but panics if an error occurs. +func (s *selector) StringX(ctx context.Context) string { + v, err := s.String(ctx) + if err != nil { + panic(err) + } + return v +} + +// Ints returns list of ints from a selector. It is only allowed when selecting one field. +func (s *selector) Ints(ctx context.Context) ([]int, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Ints is not achievable when selecting more than 1 field") + } + var v []int + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// IntsX is like Ints, but panics if an error occurs. +func (s *selector) IntsX(ctx context.Context) []int { + v, err := s.Ints(ctx) + if err != nil { + panic(err) + } + return v +} + +// Int returns a single int from a selector. It is only allowed when selecting one field. +func (s *selector) Int(ctx context.Context) (_ int, err error) { + var v []int + if v, err = s.Ints(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Ints returned %d results when one was expected", len(v)) + } + return +} + +// IntX is like Int, but panics if an error occurs. +func (s *selector) IntX(ctx context.Context) int { + v, err := s.Int(ctx) + if err != nil { + panic(err) + } + return v +} + +// Float64s returns list of float64s from a selector. It is only allowed when selecting one field. +func (s *selector) Float64s(ctx context.Context) ([]float64, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Float64s is not achievable when selecting more than 1 field") + } + var v []float64 + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// Float64sX is like Float64s, but panics if an error occurs. +func (s *selector) Float64sX(ctx context.Context) []float64 { + v, err := s.Float64s(ctx) + if err != nil { + panic(err) + } + return v +} + +// Float64 returns a single float64 from a selector. It is only allowed when selecting one field. +func (s *selector) Float64(ctx context.Context) (_ float64, err error) { + var v []float64 + if v, err = s.Float64s(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Float64s returned %d results when one was expected", len(v)) + } + return +} + +// Float64X is like Float64, but panics if an error occurs. +func (s *selector) Float64X(ctx context.Context) float64 { + v, err := s.Float64(ctx) + if err != nil { + panic(err) + } + return v +} + +// Bools returns list of bools from a selector. It is only allowed when selecting one field. +func (s *selector) Bools(ctx context.Context) ([]bool, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Bools is not achievable when selecting more than 1 field") + } + var v []bool + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// BoolsX is like Bools, but panics if an error occurs. +func (s *selector) BoolsX(ctx context.Context) []bool { + v, err := s.Bools(ctx) + if err != nil { + panic(err) + } + return v +} + +// Bool returns a single bool from a selector. It is only allowed when selecting one field. +func (s *selector) Bool(ctx context.Context) (_ bool, err error) { + var v []bool + if v, err = s.Bools(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Bools returned %d results when one was expected", len(v)) + } + return +} + +// BoolX is like Bool, but panics if an error occurs. +func (s *selector) BoolX(ctx context.Context) bool { + v, err := s.Bool(ctx) + if err != nil { + panic(err) + } + return v +} + +// withHooks invokes the builder operation with the given hooks, if any. +func withHooks[V Value, M any, PM interface { + *M + Mutation +}](ctx context.Context, exec func(context.Context) (V, error), mutation PM, hooks []Hook) (value V, err error) { + if len(hooks) == 0 { + return exec(ctx) + } + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutationT, ok := any(m).(PM) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + // Set the mutation to the builder. + *mutation = *mutationT + return exec(ctx) + }) + for i := len(hooks) - 1; i >= 0; i-- { + if hooks[i] == nil { + return value, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") + } + mut = hooks[i](mut) + } + v, err := mut.Mutate(ctx, mutation) + if err != nil { + return value, err + } + nv, ok := v.(V) + if !ok { + return value, fmt.Errorf("unexpected node type %T returned from %T", v, mutation) + } + return nv, nil +} + +// setContextOp returns a new context with the given QueryContext attached (including its op) in case it does not exist. +func setContextOp(ctx context.Context, qc *QueryContext, op string) context.Context { + if ent.QueryFromContext(ctx) == nil { + qc.Op = op + ctx = ent.NewQueryContext(ctx, qc) + } + return ctx +} + +func querierAll[V Value, Q interface { + sqlAll(context.Context, ...queryHook) (V, error) +}]() Querier { + return QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + query, ok := q.(Q) + if !ok { + return nil, fmt.Errorf("unexpected query type %T", q) + } + return query.sqlAll(ctx) + }) +} + +func querierCount[Q interface { + sqlCount(context.Context) (int, error) +}]() Querier { + return QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + query, ok := q.(Q) + if !ok { + return nil, fmt.Errorf("unexpected query type %T", q) + } + return query.sqlCount(ctx) + }) +} + +func withInterceptors[V Value](ctx context.Context, q Query, qr Querier, inters []Interceptor) (v V, err error) { + for i := len(inters) - 1; i >= 0; i-- { + qr = inters[i].Intercept(qr) + } + rv, err := qr.Query(ctx, q) + if err != nil { + return v, err + } + vt, ok := rv.(V) + if !ok { + return v, fmt.Errorf("unexpected type %T returned from %T. expected type: %T", vt, q, v) + } + return vt, nil +} + +func scanWithInterceptors[Q1 ent.Query, Q2 interface { + sqlScan(context.Context, Q1, any) error +}](ctx context.Context, rootQuery Q1, selectOrGroup Q2, inters []Interceptor, v any) error { + rv := reflect.ValueOf(v) + var qr Querier = QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + query, ok := q.(Q1) + if !ok { + return nil, fmt.Errorf("unexpected query type %T", q) + } + if err := selectOrGroup.sqlScan(ctx, query, v); err != nil { + return nil, err + } + if k := rv.Kind(); k == reflect.Pointer && rv.Elem().CanInterface() { + return rv.Elem().Interface(), nil + } + return v, nil + }) + for i := len(inters) - 1; i >= 0; i-- { + qr = inters[i].Intercept(qr) + } + vv, err := qr.Query(ctx, rootQuery) + if err != nil { + return err + } + switch rv2 := reflect.ValueOf(vv); { + case rv.IsNil(), rv2.IsNil(), rv.Kind() != reflect.Pointer: + case rv.Type() == rv2.Type(): + rv.Elem().Set(rv2.Elem()) + case rv.Elem().Type() == rv2.Type(): + rv.Elem().Set(rv2) + } + return nil +} + +// queryHook describes an internal hook for the different sqlAll methods. +type queryHook func(context.Context, *sqlgraph.QuerySpec) diff --git a/ent/enttest/enttest.go b/ent/enttest/enttest.go new file mode 100644 index 00000000..aca6734e --- /dev/null +++ b/ent/enttest/enttest.go @@ -0,0 +1,84 @@ +// Code generated by ent, DO NOT EDIT. + +package enttest + +import ( + "context" + + "github.com/unbindapp/unbind-api/ent" + // required by schema hooks. + _ "github.com/unbindapp/unbind-api/ent/runtime" + + "entgo.io/ent/dialect/sql/schema" + "github.com/unbindapp/unbind-api/ent/migrate" +) + +type ( + // TestingT is the interface that is shared between + // testing.T and testing.B and used by enttest. + TestingT interface { + FailNow() + Error(...any) + } + + // Option configures client creation. + Option func(*options) + + options struct { + opts []ent.Option + migrateOpts []schema.MigrateOption + } +) + +// WithOptions forwards options to client creation. +func WithOptions(opts ...ent.Option) Option { + return func(o *options) { + o.opts = append(o.opts, opts...) + } +} + +// WithMigrateOptions forwards options to auto migration. +func WithMigrateOptions(opts ...schema.MigrateOption) Option { + return func(o *options) { + o.migrateOpts = append(o.migrateOpts, opts...) + } +} + +func newOptions(opts []Option) *options { + o := &options{} + for _, opt := range opts { + opt(o) + } + return o +} + +// Open calls ent.Open and auto-run migration. +func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { + o := newOptions(opts) + c, err := ent.Open(driverName, dataSourceName, o.opts...) + if err != nil { + t.Error(err) + t.FailNow() + } + migrateSchema(t, c, o) + return c +} + +// NewClient calls ent.NewClient and auto-run migration. +func NewClient(t TestingT, opts ...Option) *ent.Client { + o := newOptions(opts) + c := ent.NewClient(o.opts...) + migrateSchema(t, c, o) + return c +} +func migrateSchema(t TestingT, c *ent.Client, o *options) { + tables, err := schema.CopyTables(migrate.Tables) + if err != nil { + t.Error(err) + t.FailNow() + } + if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil { + t.Error(err) + t.FailNow() + } +} diff --git a/ent/generate.go b/ent/generate.go new file mode 100644 index 00000000..107528a5 --- /dev/null +++ b/ent/generate.go @@ -0,0 +1,3 @@ +package ent + +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/execquery --feature sql/modifier --feature sql/upsert ./schema diff --git a/ent/hook/hook.go b/ent/hook/hook.go new file mode 100644 index 00000000..1d3d45e4 --- /dev/null +++ b/ent/hook/hook.go @@ -0,0 +1,199 @@ +// Code generated by ent, DO NOT EDIT. + +package hook + +import ( + "context" + "fmt" + + "github.com/unbindapp/unbind-api/ent" +) + +// The UserFunc type is an adapter to allow the use of ordinary +// function as User mutator. +type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f UserFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.UserMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m) +} + +// Condition is a hook condition function. +type Condition func(context.Context, ent.Mutation) bool + +// And groups conditions with the AND operator. +func And(first, second Condition, rest ...Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + if !first(ctx, m) || !second(ctx, m) { + return false + } + for _, cond := range rest { + if !cond(ctx, m) { + return false + } + } + return true + } +} + +// Or groups conditions with the OR operator. +func Or(first, second Condition, rest ...Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + if first(ctx, m) || second(ctx, m) { + return true + } + for _, cond := range rest { + if cond(ctx, m) { + return true + } + } + return false + } +} + +// Not negates a given condition. +func Not(cond Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + return !cond(ctx, m) + } +} + +// HasOp is a condition testing mutation operation. +func HasOp(op ent.Op) Condition { + return func(_ context.Context, m ent.Mutation) bool { + return m.Op().Is(op) + } +} + +// HasAddedFields is a condition validating `.AddedField` on fields. +func HasAddedFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if _, exists := m.AddedField(field); !exists { + return false + } + for _, field := range fields { + if _, exists := m.AddedField(field); !exists { + return false + } + } + return true + } +} + +// HasClearedFields is a condition validating `.FieldCleared` on fields. +func HasClearedFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if exists := m.FieldCleared(field); !exists { + return false + } + for _, field := range fields { + if exists := m.FieldCleared(field); !exists { + return false + } + } + return true + } +} + +// HasFields is a condition validating `.Field` on fields. +func HasFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if _, exists := m.Field(field); !exists { + return false + } + for _, field := range fields { + if _, exists := m.Field(field); !exists { + return false + } + } + return true + } +} + +// If executes the given hook under condition. +// +// hook.If(ComputeAverage, And(HasFields(...), HasAddedFields(...))) +func If(hk ent.Hook, cond Condition) ent.Hook { + return func(next ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if cond(ctx, m) { + return hk(next).Mutate(ctx, m) + } + return next.Mutate(ctx, m) + }) + } +} + +// On executes the given hook only for the given operation. +// +// hook.On(Log, ent.Delete|ent.Create) +func On(hk ent.Hook, op ent.Op) ent.Hook { + return If(hk, HasOp(op)) +} + +// Unless skips the given hook only for the given operation. +// +// hook.Unless(Log, ent.Update|ent.UpdateOne) +func Unless(hk ent.Hook, op ent.Op) ent.Hook { + return If(hk, Not(HasOp(op))) +} + +// FixedError is a hook returning a fixed error. +func FixedError(err error) ent.Hook { + return func(ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(context.Context, ent.Mutation) (ent.Value, error) { + return nil, err + }) + } +} + +// Reject returns a hook that rejects all operations that match op. +// +// func (T) Hooks() []ent.Hook { +// return []ent.Hook{ +// Reject(ent.Delete|ent.Update), +// } +// } +func Reject(op ent.Op) ent.Hook { + hk := FixedError(fmt.Errorf("%s operation is not allowed", op)) + return On(hk, op) +} + +// Chain acts as a list of hooks and is effectively immutable. +// Once created, it will always hold the same set of hooks in the same order. +type Chain struct { + hooks []ent.Hook +} + +// NewChain creates a new chain of hooks. +func NewChain(hooks ...ent.Hook) Chain { + return Chain{append([]ent.Hook(nil), hooks...)} +} + +// Hook chains the list of hooks and returns the final hook. +func (c Chain) Hook() ent.Hook { + return func(mutator ent.Mutator) ent.Mutator { + for i := len(c.hooks) - 1; i >= 0; i-- { + mutator = c.hooks[i](mutator) + } + return mutator + } +} + +// Append extends a chain, adding the specified hook +// as the last ones in the mutation flow. +func (c Chain) Append(hooks ...ent.Hook) Chain { + newHooks := make([]ent.Hook, 0, len(c.hooks)+len(hooks)) + newHooks = append(newHooks, c.hooks...) + newHooks = append(newHooks, hooks...) + return Chain{newHooks} +} + +// Extend extends a chain, adding the specified chain +// as the last ones in the mutation flow. +func (c Chain) Extend(chain Chain) Chain { + return c.Append(chain.hooks...) +} diff --git a/ent/migrate/migrate.go b/ent/migrate/migrate.go new file mode 100644 index 00000000..1956a6bf --- /dev/null +++ b/ent/migrate/migrate.go @@ -0,0 +1,64 @@ +// Code generated by ent, DO NOT EDIT. + +package migrate + +import ( + "context" + "fmt" + "io" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql/schema" +) + +var ( + // WithGlobalUniqueID sets the universal ids options to the migration. + // If this option is enabled, ent migration will allocate a 1<<32 range + // for the ids of each entity (table). + // Note that this option cannot be applied on tables that already exist. + WithGlobalUniqueID = schema.WithGlobalUniqueID + // WithDropColumn sets the drop column option to the migration. + // If this option is enabled, ent migration will drop old columns + // that were used for both fields and edges. This defaults to false. + WithDropColumn = schema.WithDropColumn + // WithDropIndex sets the drop index option to the migration. + // If this option is enabled, ent migration will drop old indexes + // that were defined in the schema. This defaults to false. + // Note that unique constraints are defined using `UNIQUE INDEX`, + // and therefore, it's recommended to enable this option to get more + // flexibility in the schema changes. + WithDropIndex = schema.WithDropIndex + // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true. + WithForeignKeys = schema.WithForeignKeys +) + +// Schema is the API for creating, migrating and dropping a schema. +type Schema struct { + drv dialect.Driver +} + +// NewSchema creates a new schema client. +func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} } + +// Create creates all schema resources. +func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error { + return Create(ctx, s, Tables, opts...) +} + +// Create creates all table resources using the given schema driver. +func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error { + migrate, err := schema.NewMigrate(s.drv, opts...) + if err != nil { + return fmt.Errorf("ent/migrate: %w", err) + } + return migrate.Create(ctx, tables...) +} + +// WriteTo writes the schema changes to w instead of running them against the database. +// +// if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { +// log.Fatal(err) +// } +func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error { + return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...) +} diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go new file mode 100644 index 00000000..4d98ace5 --- /dev/null +++ b/ent/migrate/schema.go @@ -0,0 +1,33 @@ +// Code generated by ent, DO NOT EDIT. + +package migrate + +import ( + "entgo.io/ent/dialect/sql/schema" + "entgo.io/ent/schema/field" +) + +var ( + // UsersColumns holds the columns for the "users" table. + UsersColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID, Unique: true}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "email", Type: field.TypeString, Unique: true}, + {Name: "username", Type: field.TypeString}, + {Name: "external_id", Type: field.TypeString, Unique: true}, + } + // UsersTable holds the schema information for the "users" table. + UsersTable = &schema.Table{ + Name: "users", + Columns: UsersColumns, + PrimaryKey: []*schema.Column{UsersColumns[0]}, + } + // Tables holds all the tables in the schema. + Tables = []*schema.Table{ + UsersTable, + } +) + +func init() { +} diff --git a/ent/mutation.go b/ent/mutation.go new file mode 100644 index 00000000..a5390820 --- /dev/null +++ b/ent/mutation.go @@ -0,0 +1,577 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/predicate" + "github.com/unbindapp/unbind-api/ent/user" +) + +const ( + // Operation types. + OpCreate = ent.OpCreate + OpDelete = ent.OpDelete + OpDeleteOne = ent.OpDeleteOne + OpUpdate = ent.OpUpdate + OpUpdateOne = ent.OpUpdateOne + + // Node types. + TypeUser = "User" +) + +// UserMutation represents an operation that mutates the User nodes in the graph. +type UserMutation struct { + config + op Op + typ string + id *uuid.UUID + created_at *time.Time + updated_at *time.Time + email *string + username *string + external_id *string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*User, error) + predicates []predicate.User +} + +var _ ent.Mutation = (*UserMutation)(nil) + +// userOption allows management of the mutation configuration using functional options. +type userOption func(*UserMutation) + +// newUserMutation creates new mutation for the User entity. +func newUserMutation(c config, op Op, opts ...userOption) *UserMutation { + m := &UserMutation{ + config: c, + op: op, + typ: TypeUser, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withUserID sets the ID field of the mutation. +func withUserID(id uuid.UUID) userOption { + return func(m *UserMutation) { + var ( + err error + once sync.Once + value *User + ) + m.oldValue = func(ctx context.Context) (*User, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().User.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withUser sets the old User of the mutation. +func withUser(node *User) userOption { + return func(m *UserMutation) { + m.oldValue = func(context.Context) (*User, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m UserMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m UserMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of User entities. +func (m *UserMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *UserMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *UserMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().User.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetCreatedAt sets the "created_at" field. +func (m *UserMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *UserMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *UserMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *UserMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *UserMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *UserMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetEmail sets the "email" field. +func (m *UserMutation) SetEmail(s string) { + m.email = &s +} + +// Email returns the value of the "email" field in the mutation. +func (m *UserMutation) Email() (r string, exists bool) { + v := m.email + if v == nil { + return + } + return *v, true +} + +// OldEmail returns the old "email" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldEmail(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEmail is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEmail requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEmail: %w", err) + } + return oldValue.Email, nil +} + +// ResetEmail resets all changes to the "email" field. +func (m *UserMutation) ResetEmail() { + m.email = nil +} + +// SetUsername sets the "username" field. +func (m *UserMutation) SetUsername(s string) { + m.username = &s +} + +// Username returns the value of the "username" field in the mutation. +func (m *UserMutation) Username() (r string, exists bool) { + v := m.username + if v == nil { + return + } + return *v, true +} + +// OldUsername returns the old "username" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldUsername(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUsername is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUsername requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUsername: %w", err) + } + return oldValue.Username, nil +} + +// ResetUsername resets all changes to the "username" field. +func (m *UserMutation) ResetUsername() { + m.username = nil +} + +// SetExternalID sets the "external_id" field. +func (m *UserMutation) SetExternalID(s string) { + m.external_id = &s +} + +// ExternalID returns the value of the "external_id" field in the mutation. +func (m *UserMutation) ExternalID() (r string, exists bool) { + v := m.external_id + if v == nil { + return + } + return *v, true +} + +// OldExternalID returns the old "external_id" field's value of the User entity. +// If the User object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserMutation) OldExternalID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExternalID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExternalID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExternalID: %w", err) + } + return oldValue.ExternalID, nil +} + +// ResetExternalID resets all changes to the "external_id" field. +func (m *UserMutation) ResetExternalID() { + m.external_id = nil +} + +// Where appends a list predicates to the UserMutation builder. +func (m *UserMutation) Where(ps ...predicate.User) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the UserMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.User, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *UserMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *UserMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (User). +func (m *UserMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *UserMutation) Fields() []string { + fields := make([]string, 0, 5) + if m.created_at != nil { + fields = append(fields, user.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, user.FieldUpdatedAt) + } + if m.email != nil { + fields = append(fields, user.FieldEmail) + } + if m.username != nil { + fields = append(fields, user.FieldUsername) + } + if m.external_id != nil { + fields = append(fields, user.FieldExternalID) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *UserMutation) Field(name string) (ent.Value, bool) { + switch name { + case user.FieldCreatedAt: + return m.CreatedAt() + case user.FieldUpdatedAt: + return m.UpdatedAt() + case user.FieldEmail: + return m.Email() + case user.FieldUsername: + return m.Username() + case user.FieldExternalID: + return m.ExternalID() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case user.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case user.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case user.FieldEmail: + return m.OldEmail(ctx) + case user.FieldUsername: + return m.OldUsername(ctx) + case user.FieldExternalID: + return m.OldExternalID(ctx) + } + return nil, fmt.Errorf("unknown User field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *UserMutation) SetField(name string, value ent.Value) error { + switch name { + case user.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case user.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case user.FieldEmail: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEmail(v) + return nil + case user.FieldUsername: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUsername(v) + return nil + case user.FieldExternalID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExternalID(v) + return nil + } + return fmt.Errorf("unknown User field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *UserMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *UserMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *UserMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown User numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *UserMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *UserMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *UserMutation) ClearField(name string) error { + return fmt.Errorf("unknown User nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *UserMutation) ResetField(name string) error { + switch name { + case user.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case user.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case user.FieldEmail: + m.ResetEmail() + return nil + case user.FieldUsername: + m.ResetUsername() + return nil + case user.FieldExternalID: + m.ResetExternalID() + return nil + } + return fmt.Errorf("unknown User field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *UserMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *UserMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *UserMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *UserMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *UserMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *UserMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *UserMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown User unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *UserMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown User edge %s", name) +} diff --git a/ent/predicate/predicate.go b/ent/predicate/predicate.go new file mode 100644 index 00000000..af21dfe3 --- /dev/null +++ b/ent/predicate/predicate.go @@ -0,0 +1,10 @@ +// Code generated by ent, DO NOT EDIT. + +package predicate + +import ( + "entgo.io/ent/dialect/sql" +) + +// User is the predicate function for user builders. +type User func(*sql.Selector) diff --git a/ent/runtime.go b/ent/runtime.go new file mode 100644 index 00000000..7bc427e4 --- /dev/null +++ b/ent/runtime.go @@ -0,0 +1,38 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "time" + + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/schema" + "github.com/unbindapp/unbind-api/ent/user" +) + +// The init function reads all schema descriptors with runtime code +// (default values, validators, hooks and policies) and stitches it +// to their package variables. +func init() { + userMixin := schema.User{}.Mixin() + userMixinFields0 := userMixin[0].Fields() + _ = userMixinFields0 + userMixinFields1 := userMixin[1].Fields() + _ = userMixinFields1 + userFields := schema.User{}.Fields() + _ = userFields + // userDescCreatedAt is the schema descriptor for created_at field. + userDescCreatedAt := userMixinFields1[0].Descriptor() + // user.DefaultCreatedAt holds the default value on creation for the created_at field. + user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time) + // userDescUpdatedAt is the schema descriptor for updated_at field. + userDescUpdatedAt := userMixinFields1[1].Descriptor() + // user.DefaultUpdatedAt holds the default value on creation for the updated_at field. + user.DefaultUpdatedAt = userDescUpdatedAt.Default.(func() time.Time) + // user.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + user.UpdateDefaultUpdatedAt = userDescUpdatedAt.UpdateDefault.(func() time.Time) + // userDescID is the schema descriptor for id field. + userDescID := userMixinFields0[0].Descriptor() + // user.DefaultID holds the default value on creation for the id field. + user.DefaultID = userDescID.Default.(func() uuid.UUID) +} diff --git a/ent/runtime/runtime.go b/ent/runtime/runtime.go new file mode 100644 index 00000000..aa379c26 --- /dev/null +++ b/ent/runtime/runtime.go @@ -0,0 +1,10 @@ +// Code generated by ent, DO NOT EDIT. + +package runtime + +// The schema-stitching logic is generated in github.com/unbindapp/unbind-api/ent/runtime.go + +const ( + Version = "v0.14.2" // Version of ent codegen. + Sum = "h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0=" // Sum of ent codegen. +) diff --git a/ent/schema/mixin/pk.go b/ent/schema/mixin/pk.go new file mode 100644 index 00000000..f06c06b5 --- /dev/null +++ b/ent/schema/mixin/pk.go @@ -0,0 +1,23 @@ +package mixin + +import ( + "entgo.io/ent" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/mixin" + "github.com/google/uuid" +) + +type PKMixin struct { + mixin.Schema +} + +func (PKMixin) Fields() []ent.Field { + return []ent.Field{ + field.UUID("id", uuid.UUID{}). + Default(uuid.New). + Immutable(). + StructTag(`json:"id"`). + Unique(). + Comment("The primary key of the entity."), + } +} diff --git a/ent/schema/mixin/time.go b/ent/schema/mixin/time.go new file mode 100644 index 00000000..26207cca --- /dev/null +++ b/ent/schema/mixin/time.go @@ -0,0 +1,26 @@ +package mixin + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/mixin" +) + +type TimeMixin struct { + mixin.Schema +} + +func (TimeMixin) Fields() []ent.Field { + return []ent.Field{ + field.Time("created_at"). + Immutable(). + Default(time.Now). + Comment("The time at which the entity was created."), + field.Time("updated_at"). + Default(time.Now). + UpdateDefault(time.Now). + Comment("The time at which the entity was last updated."), + } +} diff --git a/ent/schema/user.go b/ent/schema/user.go new file mode 100644 index 00000000..35ed8ea1 --- /dev/null +++ b/ent/schema/user.go @@ -0,0 +1,36 @@ +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/schema/mixin" +) + +// User holds the schema definition for the User entity. +type User struct { + ent.Schema +} + +// Mixin of the User. +func (User) Mixin() []ent.Mixin { + return []ent.Mixin{ + mixin.PKMixin{}, + mixin.TimeMixin{}, + } +} + +// Fields of the User. +func (User) Fields() []ent.Field { + return []ent.Field{ + field.String("email").Unique(), + field.String("username"), + field.String("external_id").Unique().Comment( + "Dex subject ID", + ), + } +} + +// Edges of the User. +func (User) Edges() []ent.Edge { + return nil +} diff --git a/ent/tx.go b/ent/tx.go new file mode 100644 index 00000000..3e6f77f4 --- /dev/null +++ b/ent/tx.go @@ -0,0 +1,236 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + stdsql "database/sql" + "fmt" + "sync" + + "entgo.io/ent/dialect" +) + +// Tx is a transactional client that is created by calling Client.Tx(). +type Tx struct { + config + // User is the client for interacting with the User builders. + User *UserClient + + // lazily loaded. + client *Client + clientOnce sync.Once + // ctx lives for the life of the transaction. It is + // the same context used by the underlying connection. + ctx context.Context +} + +type ( + // Committer is the interface that wraps the Commit method. + Committer interface { + Commit(context.Context, *Tx) error + } + + // The CommitFunc type is an adapter to allow the use of ordinary + // function as a Committer. If f is a function with the appropriate + // signature, CommitFunc(f) is a Committer that calls f. + CommitFunc func(context.Context, *Tx) error + + // CommitHook defines the "commit middleware". A function that gets a Committer + // and returns a Committer. For example: + // + // hook := func(next ent.Committer) ent.Committer { + // return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error { + // // Do some stuff before. + // if err := next.Commit(ctx, tx); err != nil { + // return err + // } + // // Do some stuff after. + // return nil + // }) + // } + // + CommitHook func(Committer) Committer +) + +// Commit calls f(ctx, m). +func (f CommitFunc) Commit(ctx context.Context, tx *Tx) error { + return f(ctx, tx) +} + +// Commit commits the transaction. +func (tx *Tx) Commit() error { + txDriver := tx.config.driver.(*txDriver) + var fn Committer = CommitFunc(func(context.Context, *Tx) error { + return txDriver.tx.Commit() + }) + txDriver.mu.Lock() + hooks := append([]CommitHook(nil), txDriver.onCommit...) + txDriver.mu.Unlock() + for i := len(hooks) - 1; i >= 0; i-- { + fn = hooks[i](fn) + } + return fn.Commit(tx.ctx, tx) +} + +// OnCommit adds a hook to call on commit. +func (tx *Tx) OnCommit(f CommitHook) { + txDriver := tx.config.driver.(*txDriver) + txDriver.mu.Lock() + txDriver.onCommit = append(txDriver.onCommit, f) + txDriver.mu.Unlock() +} + +type ( + // Rollbacker is the interface that wraps the Rollback method. + Rollbacker interface { + Rollback(context.Context, *Tx) error + } + + // The RollbackFunc type is an adapter to allow the use of ordinary + // function as a Rollbacker. If f is a function with the appropriate + // signature, RollbackFunc(f) is a Rollbacker that calls f. + RollbackFunc func(context.Context, *Tx) error + + // RollbackHook defines the "rollback middleware". A function that gets a Rollbacker + // and returns a Rollbacker. For example: + // + // hook := func(next ent.Rollbacker) ent.Rollbacker { + // return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error { + // // Do some stuff before. + // if err := next.Rollback(ctx, tx); err != nil { + // return err + // } + // // Do some stuff after. + // return nil + // }) + // } + // + RollbackHook func(Rollbacker) Rollbacker +) + +// Rollback calls f(ctx, m). +func (f RollbackFunc) Rollback(ctx context.Context, tx *Tx) error { + return f(ctx, tx) +} + +// Rollback rollbacks the transaction. +func (tx *Tx) Rollback() error { + txDriver := tx.config.driver.(*txDriver) + var fn Rollbacker = RollbackFunc(func(context.Context, *Tx) error { + return txDriver.tx.Rollback() + }) + txDriver.mu.Lock() + hooks := append([]RollbackHook(nil), txDriver.onRollback...) + txDriver.mu.Unlock() + for i := len(hooks) - 1; i >= 0; i-- { + fn = hooks[i](fn) + } + return fn.Rollback(tx.ctx, tx) +} + +// OnRollback adds a hook to call on rollback. +func (tx *Tx) OnRollback(f RollbackHook) { + txDriver := tx.config.driver.(*txDriver) + txDriver.mu.Lock() + txDriver.onRollback = append(txDriver.onRollback, f) + txDriver.mu.Unlock() +} + +// Client returns a Client that binds to current transaction. +func (tx *Tx) Client() *Client { + tx.clientOnce.Do(func() { + tx.client = &Client{config: tx.config} + tx.client.init() + }) + return tx.client +} + +func (tx *Tx) init() { + tx.User = NewUserClient(tx.config) +} + +// txDriver wraps the given dialect.Tx with a nop dialect.Driver implementation. +// The idea is to support transactions without adding any extra code to the builders. +// When a builder calls to driver.Tx(), it gets the same dialect.Tx instance. +// Commit and Rollback are nop for the internal builders and the user must call one +// of them in order to commit or rollback the transaction. +// +// If a closed transaction is embedded in one of the generated entities, and the entity +// applies a query, for example: User.QueryXXX(), the query will be executed +// through the driver which created this transaction. +// +// Note that txDriver is not goroutine safe. +type txDriver struct { + // the driver we started the transaction from. + drv dialect.Driver + // tx is the underlying transaction. + tx dialect.Tx + // completion hooks. + mu sync.Mutex + onCommit []CommitHook + onRollback []RollbackHook +} + +// newTx creates a new transactional driver. +func newTx(ctx context.Context, drv dialect.Driver) (*txDriver, error) { + tx, err := drv.Tx(ctx) + if err != nil { + return nil, err + } + return &txDriver{tx: tx, drv: drv}, nil +} + +// Tx returns the transaction wrapper (txDriver) to avoid Commit or Rollback calls +// from the internal builders. Should be called only by the internal builders. +func (tx *txDriver) Tx(context.Context) (dialect.Tx, error) { return tx, nil } + +// Dialect returns the dialect of the driver we started the transaction from. +func (tx *txDriver) Dialect() string { return tx.drv.Dialect() } + +// Close is a nop close. +func (*txDriver) Close() error { return nil } + +// Commit is a nop commit for the internal builders. +// User must call `Tx.Commit` in order to commit the transaction. +func (*txDriver) Commit() error { return nil } + +// Rollback is a nop rollback for the internal builders. +// User must call `Tx.Rollback` in order to rollback the transaction. +func (*txDriver) Rollback() error { return nil } + +// Exec calls tx.Exec. +func (tx *txDriver) Exec(ctx context.Context, query string, args, v any) error { + return tx.tx.Exec(ctx, query, args, v) +} + +// Query calls tx.Query. +func (tx *txDriver) Query(ctx context.Context, query string, args, v any) error { + return tx.tx.Query(ctx, query, args, v) +} + +var _ dialect.Driver = (*txDriver)(nil) + +// ExecContext allows calling the underlying ExecContext method of the transaction if it is supported by it. +// See, database/sql#Tx.ExecContext for more information. +func (tx *txDriver) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error) { + ex, ok := tx.tx.(interface { + ExecContext(context.Context, string, ...any) (stdsql.Result, error) + }) + if !ok { + return nil, fmt.Errorf("Tx.ExecContext is not supported") + } + return ex.ExecContext(ctx, query, args...) +} + +// QueryContext allows calling the underlying QueryContext method of the transaction if it is supported by it. +// See, database/sql#Tx.QueryContext for more information. +func (tx *txDriver) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error) { + q, ok := tx.tx.(interface { + QueryContext(context.Context, string, ...any) (*stdsql.Rows, error) + }) + if !ok { + return nil, fmt.Errorf("Tx.QueryContext is not supported") + } + return q.QueryContext(ctx, query, args...) +} diff --git a/ent/user.go b/ent/user.go new file mode 100644 index 00000000..a237428e --- /dev/null +++ b/ent/user.go @@ -0,0 +1,152 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/user" +) + +// User is the model entity for the User schema. +type User struct { + config `json:"-"` + // ID of the ent. + // The primary key of the entity. + ID uuid.UUID `json:"id"` + // The time at which the entity was created. + CreatedAt time.Time `json:"created_at,omitempty"` + // The time at which the entity was last updated. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // Email holds the value of the "email" field. + Email string `json:"email,omitempty"` + // Username holds the value of the "username" field. + Username string `json:"username,omitempty"` + // Dex subject ID + ExternalID string `json:"external_id,omitempty"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*User) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case user.FieldEmail, user.FieldUsername, user.FieldExternalID: + values[i] = new(sql.NullString) + case user.FieldCreatedAt, user.FieldUpdatedAt: + values[i] = new(sql.NullTime) + case user.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the User fields. +func (u *User) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case user.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + u.ID = *value + } + case user.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + u.CreatedAt = value.Time + } + case user.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + u.UpdatedAt = value.Time + } + case user.FieldEmail: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field email", values[i]) + } else if value.Valid { + u.Email = value.String + } + case user.FieldUsername: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field username", values[i]) + } else if value.Valid { + u.Username = value.String + } + case user.FieldExternalID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field external_id", values[i]) + } else if value.Valid { + u.ExternalID = value.String + } + default: + u.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the User. +// This includes values selected through modifiers, order, etc. +func (u *User) Value(name string) (ent.Value, error) { + return u.selectValues.Get(name) +} + +// Update returns a builder for updating this User. +// Note that you need to call User.Unwrap() before calling this method if this User +// was returned from a transaction, and the transaction was committed or rolled back. +func (u *User) Update() *UserUpdateOne { + return NewUserClient(u.config).UpdateOne(u) +} + +// Unwrap unwraps the User entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (u *User) Unwrap() *User { + _tx, ok := u.config.driver.(*txDriver) + if !ok { + panic("ent: User is not a transactional entity") + } + u.config.driver = _tx.drv + return u +} + +// String implements the fmt.Stringer. +func (u *User) String() string { + var builder strings.Builder + builder.WriteString("User(") + builder.WriteString(fmt.Sprintf("id=%v, ", u.ID)) + builder.WriteString("created_at=") + builder.WriteString(u.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(u.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("email=") + builder.WriteString(u.Email) + builder.WriteString(", ") + builder.WriteString("username=") + builder.WriteString(u.Username) + builder.WriteString(", ") + builder.WriteString("external_id=") + builder.WriteString(u.ExternalID) + builder.WriteByte(')') + return builder.String() +} + +// Users is a parsable slice of User. +type Users []*User diff --git a/ent/user/user.go b/ent/user/user.go new file mode 100644 index 00000000..962f1ac5 --- /dev/null +++ b/ent/user/user.go @@ -0,0 +1,93 @@ +// Code generated by ent, DO NOT EDIT. + +package user + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the user type in the database. + Label = "user" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldEmail holds the string denoting the email field in the database. + FieldEmail = "email" + // FieldUsername holds the string denoting the username field in the database. + FieldUsername = "username" + // FieldExternalID holds the string denoting the external_id field in the database. + FieldExternalID = "external_id" + // Table holds the table name of the user in the database. + Table = "users" +) + +// Columns holds all SQL columns for user fields. +var Columns = []string{ + FieldID, + FieldCreatedAt, + FieldUpdatedAt, + FieldEmail, + FieldUsername, + FieldExternalID, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the User queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByEmail orders the results by the email field. +func ByEmail(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldEmail, opts...).ToFunc() +} + +// ByUsername orders the results by the username field. +func ByUsername(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUsername, opts...).ToFunc() +} + +// ByExternalID orders the results by the external_id field. +func ByExternalID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExternalID, opts...).ToFunc() +} diff --git a/ent/user/where.go b/ent/user/where.go new file mode 100644 index 00000000..c874e3eb --- /dev/null +++ b/ent/user/where.go @@ -0,0 +1,371 @@ +// Code generated by ent, DO NOT EDIT. + +package user + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.User { + return predicate.User(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.User { + return predicate.User(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.User { + return predicate.User(sql.FieldLTE(FieldID, id)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// Email applies equality check predicate on the "email" field. It's identical to EmailEQ. +func Email(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldEmail, v)) +} + +// Username applies equality check predicate on the "username" field. It's identical to UsernameEQ. +func Username(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldUsername, v)) +} + +// ExternalID applies equality check predicate on the "external_id" field. It's identical to ExternalIDEQ. +func ExternalID(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldExternalID, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.User { + return predicate.User(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.User { + return predicate.User(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.User { + return predicate.User(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.User { + return predicate.User(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.User { + return predicate.User(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.User { + return predicate.User(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.User { + return predicate.User(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.User { + return predicate.User(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.User { + return predicate.User(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.User { + return predicate.User(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// EmailEQ applies the EQ predicate on the "email" field. +func EmailEQ(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldEmail, v)) +} + +// EmailNEQ applies the NEQ predicate on the "email" field. +func EmailNEQ(v string) predicate.User { + return predicate.User(sql.FieldNEQ(FieldEmail, v)) +} + +// EmailIn applies the In predicate on the "email" field. +func EmailIn(vs ...string) predicate.User { + return predicate.User(sql.FieldIn(FieldEmail, vs...)) +} + +// EmailNotIn applies the NotIn predicate on the "email" field. +func EmailNotIn(vs ...string) predicate.User { + return predicate.User(sql.FieldNotIn(FieldEmail, vs...)) +} + +// EmailGT applies the GT predicate on the "email" field. +func EmailGT(v string) predicate.User { + return predicate.User(sql.FieldGT(FieldEmail, v)) +} + +// EmailGTE applies the GTE predicate on the "email" field. +func EmailGTE(v string) predicate.User { + return predicate.User(sql.FieldGTE(FieldEmail, v)) +} + +// EmailLT applies the LT predicate on the "email" field. +func EmailLT(v string) predicate.User { + return predicate.User(sql.FieldLT(FieldEmail, v)) +} + +// EmailLTE applies the LTE predicate on the "email" field. +func EmailLTE(v string) predicate.User { + return predicate.User(sql.FieldLTE(FieldEmail, v)) +} + +// EmailContains applies the Contains predicate on the "email" field. +func EmailContains(v string) predicate.User { + return predicate.User(sql.FieldContains(FieldEmail, v)) +} + +// EmailHasPrefix applies the HasPrefix predicate on the "email" field. +func EmailHasPrefix(v string) predicate.User { + return predicate.User(sql.FieldHasPrefix(FieldEmail, v)) +} + +// EmailHasSuffix applies the HasSuffix predicate on the "email" field. +func EmailHasSuffix(v string) predicate.User { + return predicate.User(sql.FieldHasSuffix(FieldEmail, v)) +} + +// EmailEqualFold applies the EqualFold predicate on the "email" field. +func EmailEqualFold(v string) predicate.User { + return predicate.User(sql.FieldEqualFold(FieldEmail, v)) +} + +// EmailContainsFold applies the ContainsFold predicate on the "email" field. +func EmailContainsFold(v string) predicate.User { + return predicate.User(sql.FieldContainsFold(FieldEmail, v)) +} + +// UsernameEQ applies the EQ predicate on the "username" field. +func UsernameEQ(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldUsername, v)) +} + +// UsernameNEQ applies the NEQ predicate on the "username" field. +func UsernameNEQ(v string) predicate.User { + return predicate.User(sql.FieldNEQ(FieldUsername, v)) +} + +// UsernameIn applies the In predicate on the "username" field. +func UsernameIn(vs ...string) predicate.User { + return predicate.User(sql.FieldIn(FieldUsername, vs...)) +} + +// UsernameNotIn applies the NotIn predicate on the "username" field. +func UsernameNotIn(vs ...string) predicate.User { + return predicate.User(sql.FieldNotIn(FieldUsername, vs...)) +} + +// UsernameGT applies the GT predicate on the "username" field. +func UsernameGT(v string) predicate.User { + return predicate.User(sql.FieldGT(FieldUsername, v)) +} + +// UsernameGTE applies the GTE predicate on the "username" field. +func UsernameGTE(v string) predicate.User { + return predicate.User(sql.FieldGTE(FieldUsername, v)) +} + +// UsernameLT applies the LT predicate on the "username" field. +func UsernameLT(v string) predicate.User { + return predicate.User(sql.FieldLT(FieldUsername, v)) +} + +// UsernameLTE applies the LTE predicate on the "username" field. +func UsernameLTE(v string) predicate.User { + return predicate.User(sql.FieldLTE(FieldUsername, v)) +} + +// UsernameContains applies the Contains predicate on the "username" field. +func UsernameContains(v string) predicate.User { + return predicate.User(sql.FieldContains(FieldUsername, v)) +} + +// UsernameHasPrefix applies the HasPrefix predicate on the "username" field. +func UsernameHasPrefix(v string) predicate.User { + return predicate.User(sql.FieldHasPrefix(FieldUsername, v)) +} + +// UsernameHasSuffix applies the HasSuffix predicate on the "username" field. +func UsernameHasSuffix(v string) predicate.User { + return predicate.User(sql.FieldHasSuffix(FieldUsername, v)) +} + +// UsernameEqualFold applies the EqualFold predicate on the "username" field. +func UsernameEqualFold(v string) predicate.User { + return predicate.User(sql.FieldEqualFold(FieldUsername, v)) +} + +// UsernameContainsFold applies the ContainsFold predicate on the "username" field. +func UsernameContainsFold(v string) predicate.User { + return predicate.User(sql.FieldContainsFold(FieldUsername, v)) +} + +// ExternalIDEQ applies the EQ predicate on the "external_id" field. +func ExternalIDEQ(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldExternalID, v)) +} + +// ExternalIDNEQ applies the NEQ predicate on the "external_id" field. +func ExternalIDNEQ(v string) predicate.User { + return predicate.User(sql.FieldNEQ(FieldExternalID, v)) +} + +// ExternalIDIn applies the In predicate on the "external_id" field. +func ExternalIDIn(vs ...string) predicate.User { + return predicate.User(sql.FieldIn(FieldExternalID, vs...)) +} + +// ExternalIDNotIn applies the NotIn predicate on the "external_id" field. +func ExternalIDNotIn(vs ...string) predicate.User { + return predicate.User(sql.FieldNotIn(FieldExternalID, vs...)) +} + +// ExternalIDGT applies the GT predicate on the "external_id" field. +func ExternalIDGT(v string) predicate.User { + return predicate.User(sql.FieldGT(FieldExternalID, v)) +} + +// ExternalIDGTE applies the GTE predicate on the "external_id" field. +func ExternalIDGTE(v string) predicate.User { + return predicate.User(sql.FieldGTE(FieldExternalID, v)) +} + +// ExternalIDLT applies the LT predicate on the "external_id" field. +func ExternalIDLT(v string) predicate.User { + return predicate.User(sql.FieldLT(FieldExternalID, v)) +} + +// ExternalIDLTE applies the LTE predicate on the "external_id" field. +func ExternalIDLTE(v string) predicate.User { + return predicate.User(sql.FieldLTE(FieldExternalID, v)) +} + +// ExternalIDContains applies the Contains predicate on the "external_id" field. +func ExternalIDContains(v string) predicate.User { + return predicate.User(sql.FieldContains(FieldExternalID, v)) +} + +// ExternalIDHasPrefix applies the HasPrefix predicate on the "external_id" field. +func ExternalIDHasPrefix(v string) predicate.User { + return predicate.User(sql.FieldHasPrefix(FieldExternalID, v)) +} + +// ExternalIDHasSuffix applies the HasSuffix predicate on the "external_id" field. +func ExternalIDHasSuffix(v string) predicate.User { + return predicate.User(sql.FieldHasSuffix(FieldExternalID, v)) +} + +// ExternalIDEqualFold applies the EqualFold predicate on the "external_id" field. +func ExternalIDEqualFold(v string) predicate.User { + return predicate.User(sql.FieldEqualFold(FieldExternalID, v)) +} + +// ExternalIDContainsFold applies the ContainsFold predicate on the "external_id" field. +func ExternalIDContainsFold(v string) predicate.User { + return predicate.User(sql.FieldContainsFold(FieldExternalID, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.User) predicate.User { + return predicate.User(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.User) predicate.User { + return predicate.User(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.User) predicate.User { + return predicate.User(sql.NotPredicates(p)) +} diff --git a/ent/user_create.go b/ent/user_create.go new file mode 100644 index 00000000..27b48e0b --- /dev/null +++ b/ent/user_create.go @@ -0,0 +1,711 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/user" +) + +// UserCreate is the builder for creating a User entity. +type UserCreate struct { + config + mutation *UserMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetCreatedAt sets the "created_at" field. +func (uc *UserCreate) SetCreatedAt(t time.Time) *UserCreate { + uc.mutation.SetCreatedAt(t) + return uc +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (uc *UserCreate) SetNillableCreatedAt(t *time.Time) *UserCreate { + if t != nil { + uc.SetCreatedAt(*t) + } + return uc +} + +// SetUpdatedAt sets the "updated_at" field. +func (uc *UserCreate) SetUpdatedAt(t time.Time) *UserCreate { + uc.mutation.SetUpdatedAt(t) + return uc +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (uc *UserCreate) SetNillableUpdatedAt(t *time.Time) *UserCreate { + if t != nil { + uc.SetUpdatedAt(*t) + } + return uc +} + +// SetEmail sets the "email" field. +func (uc *UserCreate) SetEmail(s string) *UserCreate { + uc.mutation.SetEmail(s) + return uc +} + +// SetUsername sets the "username" field. +func (uc *UserCreate) SetUsername(s string) *UserCreate { + uc.mutation.SetUsername(s) + return uc +} + +// SetExternalID sets the "external_id" field. +func (uc *UserCreate) SetExternalID(s string) *UserCreate { + uc.mutation.SetExternalID(s) + return uc +} + +// SetID sets the "id" field. +func (uc *UserCreate) SetID(u uuid.UUID) *UserCreate { + uc.mutation.SetID(u) + return uc +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (uc *UserCreate) SetNillableID(u *uuid.UUID) *UserCreate { + if u != nil { + uc.SetID(*u) + } + return uc +} + +// Mutation returns the UserMutation object of the builder. +func (uc *UserCreate) Mutation() *UserMutation { + return uc.mutation +} + +// Save creates the User in the database. +func (uc *UserCreate) Save(ctx context.Context) (*User, error) { + uc.defaults() + return withHooks(ctx, uc.sqlSave, uc.mutation, uc.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (uc *UserCreate) SaveX(ctx context.Context) *User { + v, err := uc.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (uc *UserCreate) Exec(ctx context.Context) error { + _, err := uc.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (uc *UserCreate) ExecX(ctx context.Context) { + if err := uc.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (uc *UserCreate) defaults() { + if _, ok := uc.mutation.CreatedAt(); !ok { + v := user.DefaultCreatedAt() + uc.mutation.SetCreatedAt(v) + } + if _, ok := uc.mutation.UpdatedAt(); !ok { + v := user.DefaultUpdatedAt() + uc.mutation.SetUpdatedAt(v) + } + if _, ok := uc.mutation.ID(); !ok { + v := user.DefaultID() + uc.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (uc *UserCreate) check() error { + if _, ok := uc.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "User.created_at"`)} + } + if _, ok := uc.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "User.updated_at"`)} + } + if _, ok := uc.mutation.Email(); !ok { + return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "User.email"`)} + } + if _, ok := uc.mutation.Username(); !ok { + return &ValidationError{Name: "username", err: errors.New(`ent: missing required field "User.username"`)} + } + if _, ok := uc.mutation.ExternalID(); !ok { + return &ValidationError{Name: "external_id", err: errors.New(`ent: missing required field "User.external_id"`)} + } + return nil +} + +func (uc *UserCreate) sqlSave(ctx context.Context) (*User, error) { + if err := uc.check(); err != nil { + return nil, err + } + _node, _spec := uc.createSpec() + if err := sqlgraph.CreateNode(ctx, uc.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + uc.mutation.id = &_node.ID + uc.mutation.done = true + return _node, nil +} + +func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { + var ( + _node = &User{config: uc.config} + _spec = sqlgraph.NewCreateSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = uc.conflict + if id, ok := uc.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := uc.mutation.CreatedAt(); ok { + _spec.SetField(user.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := uc.mutation.UpdatedAt(); ok { + _spec.SetField(user.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := uc.mutation.Email(); ok { + _spec.SetField(user.FieldEmail, field.TypeString, value) + _node.Email = value + } + if value, ok := uc.mutation.Username(); ok { + _spec.SetField(user.FieldUsername, field.TypeString, value) + _node.Username = value + } + if value, ok := uc.mutation.ExternalID(); ok { + _spec.SetField(user.FieldExternalID, field.TypeString, value) + _node.ExternalID = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.User.Create(). +// SetCreatedAt(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (uc *UserCreate) OnConflict(opts ...sql.ConflictOption) *UserUpsertOne { + uc.conflict = opts + return &UserUpsertOne{ + create: uc, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (uc *UserCreate) OnConflictColumns(columns ...string) *UserUpsertOne { + uc.conflict = append(uc.conflict, sql.ConflictColumns(columns...)) + return &UserUpsertOne{ + create: uc, + } +} + +type ( + // UserUpsertOne is the builder for "upsert"-ing + // one User node. + UserUpsertOne struct { + create *UserCreate + } + + // UserUpsert is the "OnConflict" setter. + UserUpsert struct { + *sql.UpdateSet + } +) + +// SetUpdatedAt sets the "updated_at" field. +func (u *UserUpsert) SetUpdatedAt(v time.Time) *UserUpsert { + u.Set(user.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *UserUpsert) UpdateUpdatedAt() *UserUpsert { + u.SetExcluded(user.FieldUpdatedAt) + return u +} + +// SetEmail sets the "email" field. +func (u *UserUpsert) SetEmail(v string) *UserUpsert { + u.Set(user.FieldEmail, v) + return u +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsert) UpdateEmail() *UserUpsert { + u.SetExcluded(user.FieldEmail) + return u +} + +// SetUsername sets the "username" field. +func (u *UserUpsert) SetUsername(v string) *UserUpsert { + u.Set(user.FieldUsername, v) + return u +} + +// UpdateUsername sets the "username" field to the value that was provided on create. +func (u *UserUpsert) UpdateUsername() *UserUpsert { + u.SetExcluded(user.FieldUsername) + return u +} + +// SetExternalID sets the "external_id" field. +func (u *UserUpsert) SetExternalID(v string) *UserUpsert { + u.Set(user.FieldExternalID, v) + return u +} + +// UpdateExternalID sets the "external_id" field to the value that was provided on create. +func (u *UserUpsert) UpdateExternalID() *UserUpsert { + u.SetExcluded(user.FieldExternalID) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(user.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserUpsertOne) UpdateNewValues() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(user.FieldID) + } + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(user.FieldCreatedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserUpsertOne) Ignore() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserUpsertOne) DoNothing() *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserCreate.OnConflict +// documentation for more info. +func (u *UserUpsertOne) Update(set func(*UserUpsert)) *UserUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *UserUpsertOne) SetUpdatedAt(v time.Time) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateUpdatedAt() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetEmail sets the "email" field. +func (u *UserUpsertOne) SetEmail(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateEmail() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateEmail() + }) +} + +// SetUsername sets the "username" field. +func (u *UserUpsertOne) SetUsername(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetUsername(v) + }) +} + +// UpdateUsername sets the "username" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateUsername() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateUsername() + }) +} + +// SetExternalID sets the "external_id" field. +func (u *UserUpsertOne) SetExternalID(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetExternalID(v) + }) +} + +// UpdateExternalID sets the "external_id" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateExternalID() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateExternalID() + }) +} + +// Exec executes the query. +func (u *UserUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *UserUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: UserUpsertOne.ID is not supported by MySQL driver. Use UserUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *UserUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// UserCreateBulk is the builder for creating many User entities in bulk. +type UserCreateBulk struct { + config + err error + builders []*UserCreate + conflict []sql.ConflictOption +} + +// Save creates the User entities in the database. +func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) { + if ucb.err != nil { + return nil, ucb.err + } + specs := make([]*sqlgraph.CreateSpec, len(ucb.builders)) + nodes := make([]*User, len(ucb.builders)) + mutators := make([]Mutator, len(ucb.builders)) + for i := range ucb.builders { + func(i int, root context.Context) { + builder := ucb.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*UserMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, ucb.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = ucb.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, ucb.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, ucb.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (ucb *UserCreateBulk) SaveX(ctx context.Context) []*User { + v, err := ucb.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (ucb *UserCreateBulk) Exec(ctx context.Context) error { + _, err := ucb.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (ucb *UserCreateBulk) ExecX(ctx context.Context) { + if err := ucb.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.User.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.UserUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (ucb *UserCreateBulk) OnConflict(opts ...sql.ConflictOption) *UserUpsertBulk { + ucb.conflict = opts + return &UserUpsertBulk{ + create: ucb, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (ucb *UserCreateBulk) OnConflictColumns(columns ...string) *UserUpsertBulk { + ucb.conflict = append(ucb.conflict, sql.ConflictColumns(columns...)) + return &UserUpsertBulk{ + create: ucb, + } +} + +// UserUpsertBulk is the builder for "upsert"-ing +// a bulk of User nodes. +type UserUpsertBulk struct { + create *UserCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(user.FieldID) +// }), +// ). +// Exec(ctx) +func (u *UserUpsertBulk) UpdateNewValues() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(user.FieldID) + } + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(user.FieldCreatedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.User.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *UserUpsertBulk) Ignore() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *UserUpsertBulk) DoNothing() *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the UserCreateBulk.OnConflict +// documentation for more info. +func (u *UserUpsertBulk) Update(set func(*UserUpsert)) *UserUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&UserUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *UserUpsertBulk) SetUpdatedAt(v time.Time) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateUpdatedAt() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetEmail sets the "email" field. +func (u *UserUpsertBulk) SetEmail(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetEmail(v) + }) +} + +// UpdateEmail sets the "email" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateEmail() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateEmail() + }) +} + +// SetUsername sets the "username" field. +func (u *UserUpsertBulk) SetUsername(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetUsername(v) + }) +} + +// UpdateUsername sets the "username" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateUsername() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateUsername() + }) +} + +// SetExternalID sets the "external_id" field. +func (u *UserUpsertBulk) SetExternalID(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetExternalID(v) + }) +} + +// UpdateExternalID sets the "external_id" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateExternalID() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateExternalID() + }) +} + +// Exec executes the query. +func (u *UserUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the UserCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for UserCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *UserUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/user_delete.go b/ent/user_delete.go new file mode 100644 index 00000000..aeaae13d --- /dev/null +++ b/ent/user_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/predicate" + "github.com/unbindapp/unbind-api/ent/user" +) + +// UserDelete is the builder for deleting a User entity. +type UserDelete struct { + config + hooks []Hook + mutation *UserMutation +} + +// Where appends a list predicates to the UserDelete builder. +func (ud *UserDelete) Where(ps ...predicate.User) *UserDelete { + ud.mutation.Where(ps...) + return ud +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (ud *UserDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, ud.sqlExec, ud.mutation, ud.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (ud *UserDelete) ExecX(ctx context.Context) int { + n, err := ud.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (ud *UserDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) + if ps := ud.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, ud.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + ud.mutation.done = true + return affected, err +} + +// UserDeleteOne is the builder for deleting a single User entity. +type UserDeleteOne struct { + ud *UserDelete +} + +// Where appends a list predicates to the UserDelete builder. +func (udo *UserDeleteOne) Where(ps ...predicate.User) *UserDeleteOne { + udo.ud.mutation.Where(ps...) + return udo +} + +// Exec executes the deletion query. +func (udo *UserDeleteOne) Exec(ctx context.Context) error { + n, err := udo.ud.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{user.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (udo *UserDeleteOne) ExecX(ctx context.Context) { + if err := udo.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/user_query.go b/ent/user_query.go new file mode 100644 index 00000000..50498e7e --- /dev/null +++ b/ent/user_query.go @@ -0,0 +1,551 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/predicate" + "github.com/unbindapp/unbind-api/ent/user" +) + +// UserQuery is the builder for querying User entities. +type UserQuery struct { + config + ctx *QueryContext + order []user.OrderOption + inters []Interceptor + predicates []predicate.User + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the UserQuery builder. +func (uq *UserQuery) Where(ps ...predicate.User) *UserQuery { + uq.predicates = append(uq.predicates, ps...) + return uq +} + +// Limit the number of records to be returned by this query. +func (uq *UserQuery) Limit(limit int) *UserQuery { + uq.ctx.Limit = &limit + return uq +} + +// Offset to start from. +func (uq *UserQuery) Offset(offset int) *UserQuery { + uq.ctx.Offset = &offset + return uq +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (uq *UserQuery) Unique(unique bool) *UserQuery { + uq.ctx.Unique = &unique + return uq +} + +// Order specifies how the records should be ordered. +func (uq *UserQuery) Order(o ...user.OrderOption) *UserQuery { + uq.order = append(uq.order, o...) + return uq +} + +// First returns the first User entity from the query. +// Returns a *NotFoundError when no User was found. +func (uq *UserQuery) First(ctx context.Context) (*User, error) { + nodes, err := uq.Limit(1).All(setContextOp(ctx, uq.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{user.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (uq *UserQuery) FirstX(ctx context.Context) *User { + node, err := uq.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first User ID from the query. +// Returns a *NotFoundError when no User ID was found. +func (uq *UserQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = uq.Limit(1).IDs(setContextOp(ctx, uq.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{user.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (uq *UserQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := uq.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single User entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one User entity is found. +// Returns a *NotFoundError when no User entities are found. +func (uq *UserQuery) Only(ctx context.Context) (*User, error) { + nodes, err := uq.Limit(2).All(setContextOp(ctx, uq.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{user.Label} + default: + return nil, &NotSingularError{user.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (uq *UserQuery) OnlyX(ctx context.Context) *User { + node, err := uq.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only User ID in the query. +// Returns a *NotSingularError when more than one User ID is found. +// Returns a *NotFoundError when no entities are found. +func (uq *UserQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = uq.Limit(2).IDs(setContextOp(ctx, uq.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{user.Label} + default: + err = &NotSingularError{user.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (uq *UserQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := uq.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Users. +func (uq *UserQuery) All(ctx context.Context) ([]*User, error) { + ctx = setContextOp(ctx, uq.ctx, ent.OpQueryAll) + if err := uq.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*User, *UserQuery]() + return withInterceptors[[]*User](ctx, uq, qr, uq.inters) +} + +// AllX is like All, but panics if an error occurs. +func (uq *UserQuery) AllX(ctx context.Context) []*User { + nodes, err := uq.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of User IDs. +func (uq *UserQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if uq.ctx.Unique == nil && uq.path != nil { + uq.Unique(true) + } + ctx = setContextOp(ctx, uq.ctx, ent.OpQueryIDs) + if err = uq.Select(user.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (uq *UserQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := uq.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (uq *UserQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, uq.ctx, ent.OpQueryCount) + if err := uq.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, uq, querierCount[*UserQuery](), uq.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (uq *UserQuery) CountX(ctx context.Context) int { + count, err := uq.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (uq *UserQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, uq.ctx, ent.OpQueryExist) + switch _, err := uq.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (uq *UserQuery) ExistX(ctx context.Context) bool { + exist, err := uq.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the UserQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (uq *UserQuery) Clone() *UserQuery { + if uq == nil { + return nil + } + return &UserQuery{ + config: uq.config, + ctx: uq.ctx.Clone(), + order: append([]user.OrderOption{}, uq.order...), + inters: append([]Interceptor{}, uq.inters...), + predicates: append([]predicate.User{}, uq.predicates...), + // clone intermediate query. + sql: uq.sql.Clone(), + path: uq.path, + modifiers: append([]func(*sql.Selector){}, uq.modifiers...), + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.User.Query(). +// GroupBy(user.FieldCreatedAt). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy { + uq.ctx.Fields = append([]string{field}, fields...) + grbuild := &UserGroupBy{build: uq} + grbuild.flds = &uq.ctx.Fields + grbuild.label = user.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// } +// +// client.User.Query(). +// Select(user.FieldCreatedAt). +// Scan(ctx, &v) +func (uq *UserQuery) Select(fields ...string) *UserSelect { + uq.ctx.Fields = append(uq.ctx.Fields, fields...) + sbuild := &UserSelect{UserQuery: uq} + sbuild.label = user.Label + sbuild.flds, sbuild.scan = &uq.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a UserSelect configured with the given aggregations. +func (uq *UserQuery) Aggregate(fns ...AggregateFunc) *UserSelect { + return uq.Select().Aggregate(fns...) +} + +func (uq *UserQuery) prepareQuery(ctx context.Context) error { + for _, inter := range uq.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, uq); err != nil { + return err + } + } + } + for _, f := range uq.ctx.Fields { + if !user.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if uq.path != nil { + prev, err := uq.path(ctx) + if err != nil { + return err + } + uq.sql = prev + } + return nil +} + +func (uq *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, error) { + var ( + nodes = []*User{} + _spec = uq.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*User).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &User{config: uq.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(uq.modifiers) > 0 { + _spec.Modifiers = uq.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, uq.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (uq *UserQuery) sqlCount(ctx context.Context) (int, error) { + _spec := uq.querySpec() + if len(uq.modifiers) > 0 { + _spec.Modifiers = uq.modifiers + } + _spec.Node.Columns = uq.ctx.Fields + if len(uq.ctx.Fields) > 0 { + _spec.Unique = uq.ctx.Unique != nil && *uq.ctx.Unique + } + return sqlgraph.CountNodes(ctx, uq.driver, _spec) +} + +func (uq *UserQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) + _spec.From = uq.sql + if unique := uq.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if uq.path != nil { + _spec.Unique = true + } + if fields := uq.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, user.FieldID) + for i := range fields { + if fields[i] != user.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := uq.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := uq.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := uq.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := uq.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (uq *UserQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(uq.driver.Dialect()) + t1 := builder.Table(user.Table) + columns := uq.ctx.Fields + if len(columns) == 0 { + columns = user.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if uq.sql != nil { + selector = uq.sql + selector.Select(selector.Columns(columns...)...) + } + if uq.ctx.Unique != nil && *uq.ctx.Unique { + selector.Distinct() + } + for _, m := range uq.modifiers { + m(selector) + } + for _, p := range uq.predicates { + p(selector) + } + for _, p := range uq.order { + p(selector) + } + if offset := uq.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := uq.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (uq *UserQuery) Modify(modifiers ...func(s *sql.Selector)) *UserSelect { + uq.modifiers = append(uq.modifiers, modifiers...) + return uq.Select() +} + +// UserGroupBy is the group-by builder for User entities. +type UserGroupBy struct { + selector + build *UserQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (ugb *UserGroupBy) Aggregate(fns ...AggregateFunc) *UserGroupBy { + ugb.fns = append(ugb.fns, fns...) + return ugb +} + +// Scan applies the selector query and scans the result into the given value. +func (ugb *UserGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, ugb.build.ctx, ent.OpQueryGroupBy) + if err := ugb.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserQuery, *UserGroupBy](ctx, ugb.build, ugb, ugb.build.inters, v) +} + +func (ugb *UserGroupBy) sqlScan(ctx context.Context, root *UserQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(ugb.fns)) + for _, fn := range ugb.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*ugb.flds)+len(ugb.fns)) + for _, f := range *ugb.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*ugb.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := ugb.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// UserSelect is the builder for selecting fields of User entities. +type UserSelect struct { + *UserQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (us *UserSelect) Aggregate(fns ...AggregateFunc) *UserSelect { + us.fns = append(us.fns, fns...) + return us +} + +// Scan applies the selector query and scans the result into the given value. +func (us *UserSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, us.ctx, ent.OpQuerySelect) + if err := us.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserQuery, *UserSelect](ctx, us.UserQuery, us, us.inters, v) +} + +func (us *UserSelect) sqlScan(ctx context.Context, root *UserQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(us.fns)) + for _, fn := range us.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*us.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := us.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (us *UserSelect) Modify(modifiers ...func(s *sql.Selector)) *UserSelect { + us.modifiers = append(us.modifiers, modifiers...) + return us +} diff --git a/ent/user_update.go b/ent/user_update.go new file mode 100644 index 00000000..f803c9e4 --- /dev/null +++ b/ent/user_update.go @@ -0,0 +1,330 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/predicate" + "github.com/unbindapp/unbind-api/ent/user" +) + +// UserUpdate is the builder for updating User entities. +type UserUpdate struct { + config + hooks []Hook + mutation *UserMutation + modifiers []func(*sql.UpdateBuilder) +} + +// Where appends a list predicates to the UserUpdate builder. +func (uu *UserUpdate) Where(ps ...predicate.User) *UserUpdate { + uu.mutation.Where(ps...) + return uu +} + +// SetUpdatedAt sets the "updated_at" field. +func (uu *UserUpdate) SetUpdatedAt(t time.Time) *UserUpdate { + uu.mutation.SetUpdatedAt(t) + return uu +} + +// SetEmail sets the "email" field. +func (uu *UserUpdate) SetEmail(s string) *UserUpdate { + uu.mutation.SetEmail(s) + return uu +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (uu *UserUpdate) SetNillableEmail(s *string) *UserUpdate { + if s != nil { + uu.SetEmail(*s) + } + return uu +} + +// SetUsername sets the "username" field. +func (uu *UserUpdate) SetUsername(s string) *UserUpdate { + uu.mutation.SetUsername(s) + return uu +} + +// SetNillableUsername sets the "username" field if the given value is not nil. +func (uu *UserUpdate) SetNillableUsername(s *string) *UserUpdate { + if s != nil { + uu.SetUsername(*s) + } + return uu +} + +// SetExternalID sets the "external_id" field. +func (uu *UserUpdate) SetExternalID(s string) *UserUpdate { + uu.mutation.SetExternalID(s) + return uu +} + +// SetNillableExternalID sets the "external_id" field if the given value is not nil. +func (uu *UserUpdate) SetNillableExternalID(s *string) *UserUpdate { + if s != nil { + uu.SetExternalID(*s) + } + return uu +} + +// Mutation returns the UserMutation object of the builder. +func (uu *UserUpdate) Mutation() *UserMutation { + return uu.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (uu *UserUpdate) Save(ctx context.Context) (int, error) { + uu.defaults() + return withHooks(ctx, uu.sqlSave, uu.mutation, uu.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (uu *UserUpdate) SaveX(ctx context.Context) int { + affected, err := uu.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (uu *UserUpdate) Exec(ctx context.Context) error { + _, err := uu.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (uu *UserUpdate) ExecX(ctx context.Context) { + if err := uu.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (uu *UserUpdate) defaults() { + if _, ok := uu.mutation.UpdatedAt(); !ok { + v := user.UpdateDefaultUpdatedAt() + uu.mutation.SetUpdatedAt(v) + } +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (uu *UserUpdate) Modify(modifiers ...func(u *sql.UpdateBuilder)) *UserUpdate { + uu.modifiers = append(uu.modifiers, modifiers...) + return uu +} + +func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { + _spec := sqlgraph.NewUpdateSpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) + if ps := uu.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := uu.mutation.UpdatedAt(); ok { + _spec.SetField(user.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := uu.mutation.Email(); ok { + _spec.SetField(user.FieldEmail, field.TypeString, value) + } + if value, ok := uu.mutation.Username(); ok { + _spec.SetField(user.FieldUsername, field.TypeString, value) + } + if value, ok := uu.mutation.ExternalID(); ok { + _spec.SetField(user.FieldExternalID, field.TypeString, value) + } + _spec.AddModifiers(uu.modifiers...) + if n, err = sqlgraph.UpdateNodes(ctx, uu.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{user.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + uu.mutation.done = true + return n, nil +} + +// UserUpdateOne is the builder for updating a single User entity. +type UserUpdateOne struct { + config + fields []string + hooks []Hook + mutation *UserMutation + modifiers []func(*sql.UpdateBuilder) +} + +// SetUpdatedAt sets the "updated_at" field. +func (uuo *UserUpdateOne) SetUpdatedAt(t time.Time) *UserUpdateOne { + uuo.mutation.SetUpdatedAt(t) + return uuo +} + +// SetEmail sets the "email" field. +func (uuo *UserUpdateOne) SetEmail(s string) *UserUpdateOne { + uuo.mutation.SetEmail(s) + return uuo +} + +// SetNillableEmail sets the "email" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableEmail(s *string) *UserUpdateOne { + if s != nil { + uuo.SetEmail(*s) + } + return uuo +} + +// SetUsername sets the "username" field. +func (uuo *UserUpdateOne) SetUsername(s string) *UserUpdateOne { + uuo.mutation.SetUsername(s) + return uuo +} + +// SetNillableUsername sets the "username" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableUsername(s *string) *UserUpdateOne { + if s != nil { + uuo.SetUsername(*s) + } + return uuo +} + +// SetExternalID sets the "external_id" field. +func (uuo *UserUpdateOne) SetExternalID(s string) *UserUpdateOne { + uuo.mutation.SetExternalID(s) + return uuo +} + +// SetNillableExternalID sets the "external_id" field if the given value is not nil. +func (uuo *UserUpdateOne) SetNillableExternalID(s *string) *UserUpdateOne { + if s != nil { + uuo.SetExternalID(*s) + } + return uuo +} + +// Mutation returns the UserMutation object of the builder. +func (uuo *UserUpdateOne) Mutation() *UserMutation { + return uuo.mutation +} + +// Where appends a list predicates to the UserUpdate builder. +func (uuo *UserUpdateOne) Where(ps ...predicate.User) *UserUpdateOne { + uuo.mutation.Where(ps...) + return uuo +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (uuo *UserUpdateOne) Select(field string, fields ...string) *UserUpdateOne { + uuo.fields = append([]string{field}, fields...) + return uuo +} + +// Save executes the query and returns the updated User entity. +func (uuo *UserUpdateOne) Save(ctx context.Context) (*User, error) { + uuo.defaults() + return withHooks(ctx, uuo.sqlSave, uuo.mutation, uuo.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (uuo *UserUpdateOne) SaveX(ctx context.Context) *User { + node, err := uuo.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (uuo *UserUpdateOne) Exec(ctx context.Context) error { + _, err := uuo.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (uuo *UserUpdateOne) ExecX(ctx context.Context) { + if err := uuo.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (uuo *UserUpdateOne) defaults() { + if _, ok := uuo.mutation.UpdatedAt(); !ok { + v := user.UpdateDefaultUpdatedAt() + uuo.mutation.SetUpdatedAt(v) + } +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (uuo *UserUpdateOne) Modify(modifiers ...func(u *sql.UpdateBuilder)) *UserUpdateOne { + uuo.modifiers = append(uuo.modifiers, modifiers...) + return uuo +} + +func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { + _spec := sqlgraph.NewUpdateSpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID)) + id, ok := uuo.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "User.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := uuo.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, user.FieldID) + for _, f := range fields { + if !user.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != user.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := uuo.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := uuo.mutation.UpdatedAt(); ok { + _spec.SetField(user.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := uuo.mutation.Email(); ok { + _spec.SetField(user.FieldEmail, field.TypeString, value) + } + if value, ok := uuo.mutation.Username(); ok { + _spec.SetField(user.FieldUsername, field.TypeString, value) + } + if value, ok := uuo.mutation.ExternalID(); ok { + _spec.SetField(user.FieldExternalID, field.TypeString, value) + } + _spec.AddModifiers(uuo.modifiers...) + _node = &User{config: uuo.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, uuo.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{user.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + uuo.mutation.done = true + return _node, nil +} diff --git a/go.mod b/go.mod index 7c5f86d7..678cdd73 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,90 @@ module github.com/unbindapp/unbind-api -go 1.23.6 +go 1.24.0 require ( + entgo.io/ent v0.14.2 + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2 github.com/caarlos0/env/v11 v11.3.1 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 + github.com/coreos/go-oidc/v3 v3.12.0 github.com/danielgtaylor/huma/v2 v2.28.0 github.com/go-chi/chi/v5 v5.2.1 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.7.2 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.9.0 k8s.io/apimachinery v0.32.2 k8s.io/client-go v0.32.2 + modernc.org/sqlite v1.35.0 ) require ( + ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/inflect v0.19.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/hashicorp/hcl/v2 v2.13.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/vburenin/ifacemaker v1.2.1 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.30.0 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +tool github.com/vburenin/ifacemaker diff --git a/go.sum b/go.sum index 82846ecc..6affc029 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,21 @@ +ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE= +ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w= +entgo.io/ent v0.14.2 h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0= +entgo.io/ent v0.14.2/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2 h1:cvR3HpL1+ZLjuYVlAX5ZvLsG+nP+A9XkFDiynMdIw4U= +github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2/go.mod h1:YaJwQBnWUoxyqXy7TgAK8FXtFSGQdc0zmBUy+NuUdgk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= +github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= @@ -8,28 +24,38 @@ github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8 github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= +github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/danielgtaylor/huma/v2 v2.28.0 h1:W+hIT52MigO73edJNJWXU896uC99xSBWpKoE2PRyybM= github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wXiMIu94LV/cH2Ek= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= +github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -42,8 +68,22 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= +github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -52,6 +92,12 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -60,6 +106,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -69,72 +119,100 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= +github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= @@ -149,6 +227,30 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw= +modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 00000000..b7e402d5 --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,94 @@ +// internal/auth/middleware.go +package auth + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/ent/user" +) + +type AuthMiddleware struct { + verifier *oidc.IDTokenVerifier + client *ent.Client +} + +func NewAuthMiddleware(issuerURL string, clientID string, client *ent.Client) (*AuthMiddleware, error) { + provider, err := oidc.NewProvider(context.Background(), issuerURL) + if err != nil { + return nil, fmt.Errorf("failed to get provider: %v", err) + } + + return &AuthMiddleware{ + verifier: provider.Verifier(&oidc.Config{ClientID: clientID}), + client: client, + }, nil +} + +func (a *AuthMiddleware) Authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + bearerToken := strings.TrimPrefix(authHeader, "Bearer ") + token, err := a.verifier.Verify(r.Context(), bearerToken) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + var claims struct { + Email string `json:"email"` + Username string `json:"preferred_username"` + Subject string `json:"sub"` + } + if err := token.Claims(&claims); err != nil { + http.Error(w, "Failed to parse claims", http.StatusInternalServerError) + return + } + + // Get or create user using Ent + user, err := a.getOrCreateUser(r.Context(), claims.Email, claims.Username, claims.Subject) + if err != nil { + http.Error(w, "Failed to process user", http.StatusInternalServerError) + return + } + + ctx := context.WithValue(r.Context(), "user", user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (a *AuthMiddleware) getOrCreateUser(ctx context.Context, email, username, subject string) (*ent.User, error) { + // Try to find existing user + user, err := a.client.User. + Query(). + Where(user.EmailEQ(email)). + Only(ctx) + + if err != nil { + if !ent.IsNotFound(err) { + return nil, err + } + // Create new user if not found + user, err = a.client.User. + Create(). + SetEmail(email). + SetUsername(username). + SetExternalID(subject). + Save(ctx) + + if err != nil { + return nil, err + } + } + + return user, nil +} diff --git a/internal/database/config.go b/internal/database/config.go new file mode 100644 index 00000000..548a0a65 --- /dev/null +++ b/internal/database/config.go @@ -0,0 +1,77 @@ +package database + +import ( + "fmt" + "os" + + "entgo.io/ent/dialect" + "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/internal/log" +) + +type SqlDBConn interface { + DSN() string + Dialect() string + Driver() string +} + +type PostgresConn struct { + Host string + Port int + Password string + User string + DBName string +} + +func (c *PostgresConn) DSN() string { + return fmt.Sprintf("postgres://%s:%s@%s:%d/%s", c.User, c.Password, c.Host, c.Port, c.DBName) +} + +func (c *PostgresConn) Dialect() string { + return dialect.Postgres +} + +func (c *PostgresConn) Driver() string { + return "pgx" +} + +type SqliteConn struct { + FileName string + Mode string +} + +func (c *SqliteConn) DSN() string { + return fmt.Sprintf("file:%s?cache=shared&mode=%s&_fk=1", c.FileName, c.Mode) +} + +func (c *SqliteConn) Dialect() string { + return dialect.SQLite +} + +func (c *SqliteConn) Driver() string { return "sqlite" } + +// Gets the DB connection information based on environment variables +func GetSqlDbConn(cfg *config.Config, mock bool) (SqlDBConn, error) { + if mock { + return &SqliteConn{FileName: "testing", Mode: "memory"}, nil + } + // Use postgres + postgresDb := cfg.PostgresDB + postgresUser := cfg.PostgresUser + postgresPassword := cfg.PostgresPassword + postgresHost := cfg.PostgresHost + postgresPort := cfg.PostgresPort + + if postgresDb == "" || postgresUser == "" || postgresPassword == "" { + log.Error("Postgres environment variables not set, not sure what to do? so exiting") + os.Exit(1) + } + log.Infof("Using PostgreSQL database %s@%s:%d", postgresUser, postgresHost, postgresPort) + return &PostgresConn{ + Host: postgresHost, + Port: postgresPort, + Password: postgresPassword, + User: postgresUser, + DBName: postgresDb, + }, nil +} diff --git a/internal/database/config_test.go b/internal/database/config_test.go new file mode 100644 index 00000000..fc74fa0d --- /dev/null +++ b/internal/database/config_test.go @@ -0,0 +1,33 @@ +package database + +import ( + "testing" + + "entgo.io/ent/dialect" + "github.com/stretchr/testify/assert" + "github.com/unbindapp/unbind-api/config" +) + +func TestGetSqlDbConnPostgres(t *testing.T) { + conn, err := GetSqlDbConn(&config.Config{ + PostgresDB: "pippin", + PostgresHost: "127.0.0.1", + PostgresPassword: "password", + PostgresPort: 5432, + PostgresUser: "user", + }, false) + assert.Nil(t, err) + + assert.Equal(t, "postgres://user:password@127.0.0.1:5432/pippin", conn.DSN()) + assert.Equal(t, dialect.Postgres, conn.Dialect()) + assert.Equal(t, "pgx", conn.Driver()) +} + +func TestGetSqlDbConnMock(t *testing.T) { + conn, err := GetSqlDbConn(nil, true) + assert.Nil(t, err) + + assert.Equal(t, "file:testing?cache=shared&mode=memory&_fk=1", conn.DSN()) + assert.Equal(t, "sqlite3", conn.Dialect()) + assert.Equal(t, "sqlite", conn.Driver()) +} diff --git a/internal/database/entclient.go b/internal/database/entclient.go new file mode 100644 index 00000000..ce57d89e --- /dev/null +++ b/internal/database/entclient.go @@ -0,0 +1,20 @@ +package database + +import ( + "database/sql" + + entsql "entgo.io/ent/dialect/sql" + "github.com/appditto/pippin_nano_wallet/libs/database/ent" + _ "github.com/jackc/pgx/v5/stdlib" + _ "modernc.org/sqlite" +) + +func NewEntClient(connInfo SqlDBConn) (*ent.Client, error) { + db, err := sql.Open(connInfo.Driver(), connInfo.DSN()) + if err != nil { + return nil, err + } + + drv := entsql.OpenDB(connInfo.Dialect(), db) + return ent.NewClient(ent.Driver(drv)), nil +} diff --git a/internal/database/entclient_test.go b/internal/database/entclient_test.go new file mode 100644 index 00000000..96c18a34 --- /dev/null +++ b/internal/database/entclient_test.go @@ -0,0 +1,15 @@ +package database + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewEntClient(t *testing.T) { + dbconn, _ := GetSqlDbConn(nil, true) + + client, err := NewEntClient(dbconn) + assert.Nil(t, err) + assert.NotNil(t, client) +} diff --git a/internal/database/repository/repository.go b/internal/database/repository/repository.go new file mode 100644 index 00000000..5c33f4e9 --- /dev/null +++ b/internal/database/repository/repository.go @@ -0,0 +1,64 @@ +package repository + +import ( + "context" + "fmt" + "runtime/debug" + + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/internal/log" +) + +// Repository acts as a database interaction layer + +//go:generate go run -mod=mod github.com/vburenin/ifacemaker -f "*.go" -i RepositoryInterface -p repository -s Repository -o repository_iface.go +type Repository struct { + DB *ent.Client +} + +func NewRepository(db *ent.Client) *Repository { + return &Repository{ + DB: db, + } +} + +// WithTx runs a function in a transaction +// Usage example: +// +// if err := r.WithTx(func(tx *ent.Tx) error { +// Do stuff with tx +// return nil +// }); err != nil { +// +// Handle error +// } +func (r *Repository) WithTx(ctx context.Context, fn func(tx TxInterface) error) error { + tx, err := r.DB.Tx(ctx) + if err != nil { + return err + } + defer func() { + if v := recover(); v != nil { + log.Errorf("Panic caught in WithTX: %v", string(debug.Stack())) + tx.Rollback() + // Re-panic to pass upstream + panic(v) + } + }() + if err := fn(tx); err != nil { + if rerr := tx.Rollback(); rerr != nil { + err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr) + } + return err + } + if err := tx.Commit(); err != nil { + return fmt.Errorf("committing transaction: %w", err) + } + return nil +} + +type TxInterface interface { + Commit() error + Rollback() error + Client() *ent.Client +} diff --git a/internal/database/repository/repository_iface.go b/internal/database/repository/repository_iface.go new file mode 100644 index 00000000..bb2e642f --- /dev/null +++ b/internal/database/repository/repository_iface.go @@ -0,0 +1,22 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package repository + +import ( + "context" +) + +// RepositoryInterface ... +type RepositoryInterface interface { + // WithTx runs a function in a transaction + // Usage example: + // + // if err := r.WithTx(func(tx *ent.Tx) error { + // Do stuff with tx + // return nil + // }); err != nil { + // + // Handle error + // } + WithTx(ctx context.Context, fn func(tx TxInterface) error) error +} diff --git a/internal/database/repository/repository_test.go b/internal/database/repository/repository_test.go new file mode 100644 index 00000000..6d3a6d59 --- /dev/null +++ b/internal/database/repository/repository_test.go @@ -0,0 +1,131 @@ +package repository + +import ( + "context" + dbSql "database/sql" + "errors" + "testing" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" + "github.com/unbindapp/unbind-api/ent" + mockRepoTx "github.com/unbindapp/unbind-api/mocks/repository/tx" +) + +type RepositorySuite struct { + suite.Suite + repo RepositoryInterface + ctx context.Context + mockDb *dbSql.DB + db *ent.Client + dbMock sqlmock.Sqlmock + txMock *mockRepoTx.TxMock +} + +func (suite *RepositorySuite) SetupTest() { + suite.ctx = context.Background() + + db, mock, err := sqlmock.New() + if err != nil { + suite.T().Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + driver := sql.OpenDB(dialect.Postgres, db) + driverOption := ent.Driver(driver) + suite.dbMock = mock + suite.mockDb = db + suite.db = ent.NewClient(driverOption) + suite.repo = NewRepository(suite.db) + suite.txMock = new(mockRepoTx.TxMock) +} + +func (suite *RepositorySuite) TearDownTest() { + suite.db.Close() + suite.mockDb.Close() + suite.repo = nil +} + +func (suite *RepositorySuite) TestWithTx() { + suite.Run("WithTx Success", func() { + suite.dbMock.ExpectBegin() + suite.dbMock.ExpectCommit() + + err := suite.repo.WithTx(context.TODO(), func(_ TxInterface) error { + return nil + }) + + suite.NoError(err) + + if err := suite.dbMock.ExpectationsWereMet(); err != nil { + suite.T().Errorf("there were unfulfilled expectations: %s", err) + } + }) + + suite.Run("Commit error", func() { + suite.dbMock.ExpectBegin() + suite.dbMock.ExpectCommit().WillReturnError(errors.New("commit failed")) + + err := suite.repo.WithTx(context.TODO(), func(_ TxInterface) error { + return nil + }) + + suite.Error(err) + suite.Contains(err.Error(), "commit failed") + + if err := suite.dbMock.ExpectationsWereMet(); err != nil { + suite.T().Errorf("there were unfulfilled expectations: %s", err) + } + }) + + suite.Run("Rollback error", func() { + suite.dbMock.ExpectBegin() + suite.dbMock.ExpectRollback().WillReturnError(errors.New("rollback failed")) + + err := suite.repo.WithTx(context.TODO(), func(_ TxInterface) error { + return errors.New("trigger rollback") + }) + + suite.Error(err) + suite.Contains(err.Error(), "rollback failed") + + if err := suite.dbMock.ExpectationsWereMet(); err != nil { + suite.T().Errorf("there were unfulfilled expectations: %s", err) + } + }) + + suite.Run("Start TX error", func() { + suite.dbMock.ExpectBegin().WillReturnError(errors.New("begin failed")) + + err := suite.repo.WithTx(context.TODO(), func(_ TxInterface) error { + return nil + }) + + suite.Error(err) + suite.Contains(err.Error(), "begin failed") + + if err := suite.dbMock.ExpectationsWereMet(); err != nil { + suite.T().Errorf("there were unfulfilled expectations: %s", err) + } + }) + + suite.Run("Panic recovery", func() { + suite.dbMock.ExpectBegin() + suite.dbMock.ExpectRollback() + + suite.Panics(func() { + suite.repo.WithTx(context.TODO(), func(_ TxInterface) error { + panic("panic") + }) + }) + + if err := suite.dbMock.ExpectationsWereMet(); err != nil { + suite.T().Errorf("there were unfulfilled expectations: %s", err) + } + }) + +} + +func TestRepositorySuite(t *testing.T) { + suite.Run(t, new(RepositorySuite)) +} diff --git a/mocks/repository/repository_mock.go b/mocks/repository/repository_mock.go new file mode 100644 index 00000000..579c3616 --- /dev/null +++ b/mocks/repository/repository_mock.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.52.2. DO NOT EDIT. + +package mocks_repository + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + ent "github.com/unbindapp/unbind-api/ent" +) + +// RepositoryMock is an autogenerated mock type for the RepositoryInterface type +type RepositoryMock struct { + mock.Mock +} + +type RepositoryMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RepositoryMock) EXPECT() *RepositoryMock_Expecter { + return &RepositoryMock_Expecter{mock: &_m.Mock} +} + +// WithTx provides a mock function with given fields: ctx, fn +func (_m *RepositoryMock) WithTx(ctx context.Context, fn func(*ent.Tx) error) error { + ret := _m.Called(ctx, fn) + + if len(ret) == 0 { + panic("no return value specified for WithTx") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, func(*ent.Tx) error) error); ok { + r0 = rf(ctx, fn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RepositoryMock_WithTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithTx' +type RepositoryMock_WithTx_Call struct { + *mock.Call +} + +// WithTx is a helper method to define mock.On call +// - ctx context.Context +// - fn func(*ent.Tx) error +func (_e *RepositoryMock_Expecter) WithTx(ctx interface{}, fn interface{}) *RepositoryMock_WithTx_Call { + return &RepositoryMock_WithTx_Call{Call: _e.mock.On("WithTx", ctx, fn)} +} + +func (_c *RepositoryMock_WithTx_Call) Run(run func(ctx context.Context, fn func(*ent.Tx) error)) *RepositoryMock_WithTx_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(func(*ent.Tx) error)) + }) + return _c +} + +func (_c *RepositoryMock_WithTx_Call) Return(_a0 error) *RepositoryMock_WithTx_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RepositoryMock_WithTx_Call) RunAndReturn(run func(context.Context, func(*ent.Tx) error) error) *RepositoryMock_WithTx_Call { + _c.Call.Return(run) + return _c +} + +// NewRepositoryMock creates a new instance of RepositoryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRepositoryMock(t interface { + mock.TestingT + Cleanup(func()) +}) *RepositoryMock { + mock := &RepositoryMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/repository/tx/tx_mock.go b/mocks/repository/tx/tx_mock.go new file mode 100644 index 00000000..100ef61a --- /dev/null +++ b/mocks/repository/tx/tx_mock.go @@ -0,0 +1,172 @@ +// Code generated by mockery v2.52.2. DO NOT EDIT. + +package mocks_repository_tx + +import ( + mock "github.com/stretchr/testify/mock" + ent "github.com/unbindapp/unbind-api/ent" +) + +// TxMock is an autogenerated mock type for the TxInterface type +type TxMock struct { + mock.Mock +} + +type TxMock_Expecter struct { + mock *mock.Mock +} + +func (_m *TxMock) EXPECT() *TxMock_Expecter { + return &TxMock_Expecter{mock: &_m.Mock} +} + +// Client provides a mock function with no fields +func (_m *TxMock) Client() *ent.Client { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Client") + } + + var r0 *ent.Client + if rf, ok := ret.Get(0).(func() *ent.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ent.Client) + } + } + + return r0 +} + +// TxMock_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client' +type TxMock_Client_Call struct { + *mock.Call +} + +// Client is a helper method to define mock.On call +func (_e *TxMock_Expecter) Client() *TxMock_Client_Call { + return &TxMock_Client_Call{Call: _e.mock.On("Client")} +} + +func (_c *TxMock_Client_Call) Run(run func()) *TxMock_Client_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxMock_Client_Call) Return(_a0 *ent.Client) *TxMock_Client_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxMock_Client_Call) RunAndReturn(run func() *ent.Client) *TxMock_Client_Call { + _c.Call.Return(run) + return _c +} + +// Commit provides a mock function with no fields +func (_m *TxMock) Commit() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Commit") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxMock_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' +type TxMock_Commit_Call struct { + *mock.Call +} + +// Commit is a helper method to define mock.On call +func (_e *TxMock_Expecter) Commit() *TxMock_Commit_Call { + return &TxMock_Commit_Call{Call: _e.mock.On("Commit")} +} + +func (_c *TxMock_Commit_Call) Run(run func()) *TxMock_Commit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxMock_Commit_Call) Return(_a0 error) *TxMock_Commit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxMock_Commit_Call) RunAndReturn(run func() error) *TxMock_Commit_Call { + _c.Call.Return(run) + return _c +} + +// Rollback provides a mock function with no fields +func (_m *TxMock) Rollback() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Rollback") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxMock_Rollback_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rollback' +type TxMock_Rollback_Call struct { + *mock.Call +} + +// Rollback is a helper method to define mock.On call +func (_e *TxMock_Expecter) Rollback() *TxMock_Rollback_Call { + return &TxMock_Rollback_Call{Call: _e.mock.On("Rollback")} +} + +func (_c *TxMock_Rollback_Call) Run(run func()) *TxMock_Rollback_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxMock_Rollback_Call) Return(_a0 error) *TxMock_Rollback_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxMock_Rollback_Call) RunAndReturn(run func() error) *TxMock_Rollback_Call { + _c.Call.Return(run) + return _c +} + +// NewTxMock creates a new instance of TxMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTxMock(t interface { + mock.TestingT + Cleanup(func()) +}) *TxMock { + mock := &TxMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/scripts/clear-local-data.sh b/scripts/clear-local-data.sh new file mode 100755 index 00000000..e1ec8468 --- /dev/null +++ b/scripts/clear-local-data.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +rm -r ./.data/postgres/* +rm -r ./.data/redis/* +rm -r ./.data/authentik/* diff --git a/scripts/init-zitadel.sh b/scripts/init-zitadel.sh deleted file mode 100755 index 35e72aec..00000000 --- a/scripts/init-zitadel.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash -set -e - -# Function to base64url encode -base64url() { - base64 | tr '+/' '-_' | tr -d '=' -} - -# Wait for Zitadel to be ready -wait_for_zitadel() { - echo "Waiting for Zitadel to be ready..." - until curl -s http://zitadel:8080/healthz | grep -q "SERVING"; do - echo "Waiting for Zitadel..." - sleep 2 - done - echo "Zitadel is ready!" -} - -# Function to get access token using service account -get_access_token() { - local SA_KEY_FILE="./.data/zitadel/zitadel-admin-sa.json" - if [ ! -f "$SA_KEY_FILE" ]; then - echo "Service account key file not found at $SA_KEY_FILE" - exit 1 - fi - - echo "Reading service account key file..." - cat "$SA_KEY_FILE" - - # Extract necessary fields from the JSON key file - local KEY_ID=$(jq -r .keyId "$SA_KEY_FILE") - if [ -z "$KEY_ID" ]; then - echo "Failed to extract keyId from service account file" - exit 1 - fi - echo "Found key ID: $KEY_ID" - - local KEY=$(jq -r .key "$SA_KEY_FILE") - if [ -z "$KEY" ]; then - echo "Failed to extract key from service account file" - exit 1 - fi - echo "Found private key" - - # Create JWT token - local NOW=$(date +%s) - local EXP=$((NOW + 3600)) - - local HEADER='{"alg":"RS256","kid":"'$KEY_ID'"}' - local PAYLOAD='{"aud":["zitadel"],"exp":'$EXP',"iat":'$NOW',"iss":"zitadel-admin-sa","sub":"zitadel-admin-sa","scope":["openid","profile","email"]}' - - echo "Created header: $HEADER" - echo "Created payload: $PAYLOAD" - - # Base64URL encode header and payload - local B64_HEADER=$(echo -n "$HEADER" | base64url) - local B64_PAYLOAD=$(echo -n "$PAYLOAD" | base64url) - - # Create signature - echo "Signing JWT..." - echo "$KEY" > /tmp/private.key - local SIGNATURE=$(echo -n "$B64_HEADER.$B64_PAYLOAD" | openssl dgst -sha256 -sign /tmp/private.key | base64url) - rm /tmp/private.key - - local JWT="$B64_HEADER.$B64_PAYLOAD.$SIGNATURE" - echo "Created JWT: $JWT" - - # Get the access token - echo "Requesting access token from Zitadel..." - local RESPONSE=$(curl -v -X POST "http://zitadel:8080/oauth/v2/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ - -d "assertion=$JWT") - - echo "Token response: $RESPONSE" - local ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token) - - if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then - echo "Failed to get access token" - exit 1 - fi - - echo "$ACCESS_TOKEN" -} - -# Create project and OIDC configuration -setup_project() { - local ACCESS_TOKEN=$1 - - # Create project - echo "Creating Unbind API project..." - local PROJECT_RESPONSE=$(curl -s -X POST "http://zitadel:8080/oauth/v2/projects" \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Unbind API", - "projectRoleAssertion": true, - "projectRoleCheck": true - }') - - echo "Project creation response: $PROJECT_RESPONSE" - local PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r .id) - - if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then - echo "Failed to create project" - exit 1 - fi - - # Create OIDC application - echo "Creating OIDC application..." - local APP_RESPONSE=$(curl -s -X POST "http://zitadel:8080/oauth/v2/projects/$PROJECT_ID/apps/oidc" \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Unbind Local Dev", - "redirectUris": ["http://localhost:8089/auth/callback"], - "responseTypes": ["CODE"], - "grantTypes": ["AUTHORIZATION_CODE", "REFRESH_TOKEN"], - "appType": "USER_AGENT", - "authMethodType": "NONE", - "version": "V2" - }') - - echo "App creation response: $APP_RESPONSE" - - # Save OIDC configuration - local CLIENT_ID=$(echo "$APP_RESPONSE" | jq -r .clientId) - local CLIENT_SECRET=$(echo "$APP_RESPONSE" | jq -r .clientSecret) - - if [ -z "$CLIENT_ID" ] || [ "$CLIENT_ID" = "null" ]; then - echo "Failed to create OIDC application" - exit 1 - fi - - echo "Saving OIDC configuration..." - echo '{ - "issuer": "http://localhost:8082", - "clientId": "'$CLIENT_ID'", - "clientSecret": "'$CLIENT_SECRET'", - "redirectUri": "http://localhost:8089/auth/callback" - }' > ./.data/zitadel/oidc.json - - echo "OIDC configuration saved to ./.data/zitadel/oidc.json" -} - -main() { - wait_for_zitadel - - echo "Getting access token..." - ACCESS_TOKEN=$(get_access_token) - - if [ -z "$ACCESS_TOKEN" ]; then - echo "Failed to get access token" - exit 1 - fi - - setup_project "$ACCESS_TOKEN" - - echo "Zitadel initialization completed successfully!" -} - -main \ No newline at end of file From ff0a1bc66fefef38f7f1926cb0a24623f0946865 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 17 Feb 2025 22:15:22 +0000 Subject: [PATCH 05/22] initial login flow and callback --- .devcontainer/dex.yaml | 18 +- .devcontainer/docker-compose.yaml | 5 + cmd/main.go | 55 ++++- config/config.go | 8 +- go.mod | 1 - go.sum | 2 - internal/auth/middleware.go | 94 -------- internal/database/entclient.go | 2 +- .../database/repository/repository_iface.go | 3 + internal/database/repository/user.go | 35 +++ internal/middleware/auth.go | 47 ++++ internal/middleware/logger.go | 173 ++++++++++++++ internal/middleware/logger_test.go | 53 +++++ internal/middleware/middleware.go | 27 +++ internal/middleware/request_id.go | 98 ++++++++ internal/middleware/request_id_test.go | 71 ++++++ internal/middleware/terminal.go | 66 ++++++ internal/middleware/wrap_writer.go | 221 ++++++++++++++++++ internal/middleware/wrap_writer_test.go | 24 ++ internal/server/auth_callback.go | 47 ++++ internal/server/login.go | 43 ++++ internal/server/server.go | 11 +- internal/server/teams.go | 2 +- 23 files changed, 994 insertions(+), 112 deletions(-) delete mode 100644 internal/auth/middleware.go create mode 100644 internal/database/repository/user.go create mode 100644 internal/middleware/auth.go create mode 100644 internal/middleware/logger.go create mode 100644 internal/middleware/logger_test.go create mode 100644 internal/middleware/middleware.go create mode 100644 internal/middleware/request_id.go create mode 100644 internal/middleware/request_id_test.go create mode 100644 internal/middleware/terminal.go create mode 100644 internal/middleware/wrap_writer.go create mode 100644 internal/middleware/wrap_writer_test.go create mode 100644 internal/server/auth_callback.go create mode 100644 internal/server/login.go diff --git a/.devcontainer/dex.yaml b/.devcontainer/dex.yaml index 60464181..e5feea1a 100644 --- a/.devcontainer/dex.yaml +++ b/.devcontainer/dex.yaml @@ -1,13 +1,19 @@ -issuer: http://127.0.0.1:5556 +issuer: http://host.docker.internal:5556 storage: type: memory + web: http: 0.0.0.0:5556 +# Allow multiple issuer URLs +issuers: + - http://host.docker.internal:5556 # For container-to-container + - http://127.0.0.1:5556 # For host access + - http://localhost:5556 + enablePasswordDB: true staticPasswords: - email: "admin@example.com" - # Password is "password" hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" userID: "12345678-1234-5678-1234-567812345678" @@ -16,9 +22,9 @@ oauth2: skipApprovalScreen: true staticClients: - - id: my-go-app + - id: unbind-dev secret: supersecret - name: "Local Dev Example" + name: "Unbind Dev" redirectURIs: - - http://localhost:8089/callback - - http://127.0.0.1:8089/callback + - http://localhost:8089/auth/callback + - http://127.0.0.1:8089/auth/callback diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index 5687cfc3..005b78ac 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -42,6 +42,11 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=unbind + - DEX_ISSUER_URL=http://host.docker.internal:5556 + # Really this is for our dev container junk + - DEX_ISSUER_URL_EXTERNAL=http://localhost:5556 + - DEX_CLIENT_ID=unbind-dev + - DEX_CLIENT_SECRET=supersecret ports: - '127.0.0.1:8089:8089' volumes: diff --git a/cmd/main.go b/cmd/main.go index b3c35aa9..d93821ed 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "log" "net/http" @@ -10,15 +11,32 @@ import ( "github.com/go-chi/chi/v5" "github.com/joho/godotenv" "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/internal/database" + "github.com/unbindapp/unbind-api/internal/database/repository" "github.com/unbindapp/unbind-api/internal/kubeclient" + "github.com/unbindapp/unbind-api/internal/middleware" "github.com/unbindapp/unbind-api/internal/server" + "golang.org/x/oauth2" ) func main() { godotenv.Load() - cfg := config.NewConfig() // Initialize config + cfg := config.NewConfig() + + // Load database + dbConnInfo, err := database.GetSqlDbConn(cfg, false) + if err != nil { + log.Fatalf("Failed to get database connection info: %v", err) + } + // Initialize ent client + db, err := database.NewEntClient(dbConnInfo) + if err != nil { + log.Fatalf("Failed to create ent client: %v", err) + } + repo := repository.NewRepository(db) // Create kubernetes client kubeClient := kubeclient.NewKubeClient(cfg) @@ -26,16 +44,49 @@ func main() { // Implementation srvImpl := &server.Server{ KubeClient: kubeClient, + Cfg: cfg, + // Create an OAuth2 configuration using the Dex + OauthConfig: &oauth2.Config{ + ClientID: cfg.DexClientID, + ClientSecret: cfg.DexClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: cfg.DexIssuerUrlExternal + "/auth", + TokenURL: cfg.DexIssuerURL + "/token", + }, + // ! TODO - adjust redirect when necessary + RedirectURL: "http://localhost:8089/auth/callback", + Scopes: []string{"openid", "profile", "email"}, + }, + } + + // Create middleware + mw, err := middleware.NewMiddleware(cfg, repo) + if err != nil { + log.Fatalf("Failed to create middleware: %v", err) } // New chi router r := chi.NewRouter() + r.Use(mw.Logger) api := humachi.New(r, huma.DefaultConfig("Unbind API", "1.0.0")) // Add routes huma.Get(api, "/healthz", srvImpl.HealthCheck) - // ! TODO - auth stuff + r.Group(func(r chi.Router) { + huma.Get(api, "/auth/login", srvImpl.Login) + huma.Get(api, "/auth/callback", srvImpl.Callback) + }) + + // Protected routes + r.Group(func(r chi.Router) { + r.Use(mw.Authenticate) + + r.Get("/api/me", func(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value("user").(*ent.User) + json.NewEncoder(w).Encode(user) + }) + }) huma.Get(api, "/teams", srvImpl.ListTeams) // Start the server diff --git a/config/config.go b/config/config.go index d3b7c600..71588f05 100644 --- a/config/config.go +++ b/config/config.go @@ -12,9 +12,11 @@ type Config struct { PostgresUser string `env:"POSTGRES_USER" envDefault:"postgres"` PostgresPassword string `env:"POSTGRES_PASSWORD" envDefault:"postgres"` PostgresDB string `env:"POSTGRES_DB" envDefault:"unbind"` - // Zitadel - ZitadelClientID string `env:"ZITADEL_CLIENT_ID"` - ZitadelOidcKey string `env:"ZITADEL_OIDC_KEY"` + // Dex (OIDC provider) + DexIssuerURL string `env:"DEX_ISSUER_URL"` + DexIssuerUrlExternal string `env:"DEX_ISSUER_URL_EXTERNAL"` + DexClientID string `env:"DEX_CLIENT_ID"` + DexClientSecret string `env:"DEX_CLIENT_SECRET"` // Kubernetes config, optional - if in cluster it will use the in-cluster config KubeConfig string `env:"KUBECONFIG"` } diff --git a/go.mod b/go.mod index 678cdd73..3c7265ce 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.0 require ( entgo.io/ent v0.14.2 github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2 github.com/caarlos0/env/v11 v11.3.1 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 diff --git a/go.sum b/go.sum index 6affc029..044cf69f 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2 h1:cvR3HpL1+ZLjuYVlAX5ZvLsG+nP+A9XkFDiynMdIw4U= -github.com/appditto/pippin_nano_wallet/libs/database v0.0.0-20250128170224-2e063d17a3d2/go.mod h1:YaJwQBnWUoxyqXy7TgAK8FXtFSGQdc0zmBUy+NuUdgk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go deleted file mode 100644 index b7e402d5..00000000 --- a/internal/auth/middleware.go +++ /dev/null @@ -1,94 +0,0 @@ -// internal/auth/middleware.go -package auth - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/unbindapp/unbind-api/ent" - "github.com/unbindapp/unbind-api/ent/user" -) - -type AuthMiddleware struct { - verifier *oidc.IDTokenVerifier - client *ent.Client -} - -func NewAuthMiddleware(issuerURL string, clientID string, client *ent.Client) (*AuthMiddleware, error) { - provider, err := oidc.NewProvider(context.Background(), issuerURL) - if err != nil { - return nil, fmt.Errorf("failed to get provider: %v", err) - } - - return &AuthMiddleware{ - verifier: provider.Verifier(&oidc.Config{ClientID: clientID}), - client: client, - }, nil -} - -func (a *AuthMiddleware) Authenticate(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Authorization header required", http.StatusUnauthorized) - return - } - - bearerToken := strings.TrimPrefix(authHeader, "Bearer ") - token, err := a.verifier.Verify(r.Context(), bearerToken) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - var claims struct { - Email string `json:"email"` - Username string `json:"preferred_username"` - Subject string `json:"sub"` - } - if err := token.Claims(&claims); err != nil { - http.Error(w, "Failed to parse claims", http.StatusInternalServerError) - return - } - - // Get or create user using Ent - user, err := a.getOrCreateUser(r.Context(), claims.Email, claims.Username, claims.Subject) - if err != nil { - http.Error(w, "Failed to process user", http.StatusInternalServerError) - return - } - - ctx := context.WithValue(r.Context(), "user", user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func (a *AuthMiddleware) getOrCreateUser(ctx context.Context, email, username, subject string) (*ent.User, error) { - // Try to find existing user - user, err := a.client.User. - Query(). - Where(user.EmailEQ(email)). - Only(ctx) - - if err != nil { - if !ent.IsNotFound(err) { - return nil, err - } - // Create new user if not found - user, err = a.client.User. - Create(). - SetEmail(email). - SetUsername(username). - SetExternalID(subject). - Save(ctx) - - if err != nil { - return nil, err - } - } - - return user, nil -} diff --git a/internal/database/entclient.go b/internal/database/entclient.go index ce57d89e..a5b450ed 100644 --- a/internal/database/entclient.go +++ b/internal/database/entclient.go @@ -4,8 +4,8 @@ import ( "database/sql" entsql "entgo.io/ent/dialect/sql" - "github.com/appditto/pippin_nano_wallet/libs/database/ent" _ "github.com/jackc/pgx/v5/stdlib" + "github.com/unbindapp/unbind-api/ent" _ "modernc.org/sqlite" ) diff --git a/internal/database/repository/repository_iface.go b/internal/database/repository/repository_iface.go index bb2e642f..0922d7da 100644 --- a/internal/database/repository/repository_iface.go +++ b/internal/database/repository/repository_iface.go @@ -4,6 +4,8 @@ package repository import ( "context" + + "github.com/unbindapp/unbind-api/ent" ) // RepositoryInterface ... @@ -19,4 +21,5 @@ type RepositoryInterface interface { // Handle error // } WithTx(ctx context.Context, fn func(tx TxInterface) error) error + GetOrCreateUser(ctx context.Context, email, username, subject string) (*ent.User, error) } diff --git a/internal/database/repository/user.go b/internal/database/repository/user.go new file mode 100644 index 00000000..34158978 --- /dev/null +++ b/internal/database/repository/user.go @@ -0,0 +1,35 @@ +package repository + +import ( + "context" + + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/ent/user" +) + +func (r *Repository) GetOrCreateUser(ctx context.Context, email, username, subject string) (*ent.User, error) { + // Try to find existing user + user, err := r.DB.User. + Query(). + Where(user.EmailEQ(email)). + Only(ctx) + + if err != nil { + if !ent.IsNotFound(err) { + return nil, err + } + // Create new user if not found + user, err = r.DB.User. + Create(). + SetEmail(email). + SetUsername(username). + SetExternalID(subject). + Save(ctx) + + if err != nil { + return nil, err + } + } + + return user, nil +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 00000000..ee2714ca --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/unbindapp/unbind-api/internal/log" +) + +func (m *Middleware) Authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + bearerToken := strings.TrimPrefix(authHeader, "Bearer ") + token, err := m.verifier.Verify(r.Context(), bearerToken) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + var claims struct { + Email string `json:"email"` + Username string `json:"preferred_username"` + Subject string `json:"sub"` + } + if err := token.Claims(&claims); err != nil { + http.Error(w, "Failed to parse claims", http.StatusInternalServerError) + return + } + + // Get or create user using Ent + user, err := m.repository.GetOrCreateUser(r.Context(), claims.Email, claims.Username, claims.Subject) + if err != nil { + log.Errorf("Failed to process user: %v", err) + http.Error(w, "Failed to process user", http.StatusInternalServerError) + return + } + + ctx := context.WithValue(r.Context(), "user", user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 00000000..c67a2699 --- /dev/null +++ b/internal/middleware/logger.go @@ -0,0 +1,173 @@ +// Log middleware from go-chi +// https://github.com/go-chi/chi/blob/d32a83448b5f43e42bc96487c6b0b3667a92a2e4/middleware/logger.go +// Modified for our custom logger + +package middleware + +import ( + "bytes" + "context" + "net/http" + "runtime" + "time" + + "github.com/unbindapp/unbind-api/internal/log" +) + +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +var ( + // LogEntryCtxKey is the context.Context key to store the request log entry. + LogEntryCtxKey = &contextKey{"LogEntry"} + + // DefaultLogger is called by the Logger middleware handler to log each request. + // Its made a package-level variable so that it can be reconfigured for custom + // logging configurations. + DefaultLogger func(next http.Handler) http.Handler +) + +// Logger is a middleware that logs the start and end of each request, along +// with some useful data about what was requested, what the response status was, +// and how long it took to return. When standard output is a TTY, Logger will +// print in color, otherwise it will print in black and white. Logger prints a +// request ID if one is provided. +// +// Alternatively, look at https://github.com/goware/httplog for a more in-depth +// http logger with structured logging support. +// +// IMPORTANT NOTE: Logger should go before any other middleware that may change +// the response, such as middleware.Recoverer. Example: +// +// r := chi.NewRouter() +// r.Use(middleware.Logger) // <--<< Logger should come before Recoverer +// r.Use(middleware.Recoverer) +// r.Get("/", handler) +func (m *Middleware) Logger(next http.Handler) http.Handler { + return DefaultLogger(next) +} + +// RequestLogger returns a logger handler using a custom LogFormatter. +func RequestLogger(f LogFormatter) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + entry := f.NewLogEntry(r) + ww := NewWrapResponseWriter(w, r.ProtoMajor) + + t1 := time.Now() + defer func() { + entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), nil) + }() + + next.ServeHTTP(ww, WithLogEntry(r, entry)) + } + return http.HandlerFunc(fn) + } +} + +// LogFormatter initiates the beginning of a new LogEntry per request. +// See DefaultLogFormatter for an example implementation. +type LogFormatter interface { + NewLogEntry(r *http.Request) LogEntry +} + +// LogEntry records the final log when a request completes. +// See defaultLogEntry for an example implementation. +type LogEntry interface { + Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) +} + +// GetLogEntry returns the in-context LogEntry for a request. +func GetLogEntry(r *http.Request) LogEntry { + entry, _ := r.Context().Value(LogEntryCtxKey).(LogEntry) + return entry +} + +// WithLogEntry sets the in-context LogEntry for a request. +func WithLogEntry(r *http.Request, entry LogEntry) *http.Request { + r = r.WithContext(context.WithValue(r.Context(), LogEntryCtxKey, entry)) + return r +} + +// DefaultLogFormatter is a simple logger that implements a LogFormatter. +type DefaultLogFormatter struct { + NoColor bool +} + +// NewLogEntry creates a new LogEntry for the request. +func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry { + useColor := !l.NoColor + entry := &defaultLogEntry{ + DefaultLogFormatter: l, + request: r, + buf: &bytes.Buffer{}, + useColor: useColor, + } + + reqID := GetReqID(r.Context()) + if reqID != "" { + cW(entry.buf, useColor, nYellow, "[%s] ", reqID) + } + cW(entry.buf, useColor, nCyan, "\"") + cW(entry.buf, useColor, bMagenta, "%s ", r.Method) + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto) + + entry.buf.WriteString("from ") + entry.buf.WriteString(r.RemoteAddr) + entry.buf.WriteString(" - ") + + return entry +} + +type defaultLogEntry struct { + *DefaultLogFormatter + request *http.Request + buf *bytes.Buffer + useColor bool +} + +func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + switch { + case status < 200: + cW(l.buf, l.useColor, bBlue, "%03d", status) + case status < 300: + cW(l.buf, l.useColor, bGreen, "%03d", status) + case status < 400: + cW(l.buf, l.useColor, bCyan, "%03d", status) + case status < 500: + cW(l.buf, l.useColor, bYellow, "%03d", status) + default: + cW(l.buf, l.useColor, bRed, "%03d", status) + } + + cW(l.buf, l.useColor, bBlue, " %dB", bytes) + + l.buf.WriteString(" in ") + + elapsedMillis := elapsed.Truncate(time.Millisecond) + + if elapsed < 500*time.Millisecond { + cW(l.buf, l.useColor, nGreen, "%s", elapsedMillis) + } else if elapsed < 5*time.Second { + cW(l.buf, l.useColor, nYellow, "%s", elapsedMillis) + } else { + cW(l.buf, l.useColor, nRed, "%s", elapsedMillis) + } + + log.Infof(l.buf.String()) +} + +func init() { + color := true + if runtime.GOOS == "windows" { + color = false + } + DefaultLogger = RequestLogger(&DefaultLogFormatter{NoColor: !color}) +} diff --git a/internal/middleware/logger_test.go b/internal/middleware/logger_test.go new file mode 100644 index 00000000..f6458416 --- /dev/null +++ b/internal/middleware/logger_test.go @@ -0,0 +1,53 @@ +package middleware + +import ( + "bufio" + "bytes" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type testLoggerWriter struct { + *httptest.ResponseRecorder +} + +func (cw testLoggerWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, nil +} + +func TestRequestLogger(t *testing.T) { + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := w.(http.Hijacker) + if !ok { + t.Errorf("http.Hijacker is unavailable on the writer. add the interface methods.") + } + }) + + r := httptest.NewRequest("GET", "/", nil) + w := testLoggerWriter{ + ResponseRecorder: httptest.NewRecorder(), + } + + handler := DefaultLogger(testHandler) + handler.ServeHTTP(w, r) +} + +func TestRequestLoggerReadFrom(t *testing.T) { + data := []byte("file data") + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeContent(w, r, "file", time.Time{}, bytes.NewReader(data)) + }) + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + handler := DefaultLogger(testHandler) + handler.ServeHTTP(w, r) + + assert.Equal(t, data, w.Body.Bytes()) +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 00000000..a573941f --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "context" + "fmt" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/internal/database/repository" +) + +type Middleware struct { + verifier *oidc.IDTokenVerifier + repository repository.RepositoryInterface +} + +func NewMiddleware(cfg *config.Config, repository repository.RepositoryInterface) (*Middleware, error) { + provider, err := oidc.NewProvider(context.Background(), cfg.DexIssuerURL) + if err != nil { + return nil, fmt.Errorf("failed to get provider: %v", err) + } + + return &Middleware{ + verifier: provider.Verifier(&oidc.Config{ClientID: cfg.DexClientID}), + repository: repository, + }, nil +} diff --git a/internal/middleware/request_id.go b/internal/middleware/request_id.go new file mode 100644 index 00000000..a0ad461a --- /dev/null +++ b/internal/middleware/request_id.go @@ -0,0 +1,98 @@ +// From go-chi +// https://github.com/go-chi/chi/blob/d32a83448b5f43e42bc96487c6b0b3667a92a2e4/middleware/request_id.go +package middleware + +// Ported from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + "os" + "strings" + "sync/atomic" +) + +// Key to use when setting the request ID. +type ctxKeyRequestID int + +// RequestIDKey is the key that holds the unique request ID in a request context. +const RequestIDKey ctxKeyRequestID = 0 + +// RequestIDHeader is the name of the HTTP Header which contains the request id. +// Exported so that it can be changed by developers +var RequestIDHeader = "X-Request-Id" + +var prefix string +var reqid uint64 + +// A quick note on the statistics here: we're trying to calculate the chance that +// two randomly generated base62 prefixes will collide. We use the formula from +// http://en.wikipedia.org/wiki/Birthday_problem +// +// P[m, n] \approx 1 - e^{-m^2/2n} +// +// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server +// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$ +// +// For a $k$ character base-62 identifier, we have $n(k) = 62^k$ +// +// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for +// our purposes, and is surely more than anyone would ever need in practice -- a +// process that is rebooted a handful of times a day for a hundred years has less +// than a millionth of a percent chance of generating two colliding IDs. + +func init() { + hostname, err := os.Hostname() + if hostname == "" || err != nil { + hostname = "localhost" + } + var buf [12]byte + var b64 string + for len(b64) < 10 { + rand.Read(buf[:]) + b64 = base64.StdEncoding.EncodeToString(buf[:]) + b64 = strings.NewReplacer("+", "", "/", "").Replace(b64) + } + + prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10]) +} + +// RequestID is a middleware that injects a request ID into the context of each +// request. A request ID is a string of the form "host.example.com/random-0001", +// where "random" is a base62 random string that uniquely identifies this go +// process, and where the last number is an atomically incremented request +// counter. +func RequestID(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID := r.Header.Get(RequestIDHeader) + if requestID == "" { + myid := atomic.AddUint64(&reqid, 1) + requestID = fmt.Sprintf("%s-%06d", prefix, myid) + } + ctx = context.WithValue(ctx, RequestIDKey, requestID) + next.ServeHTTP(w, r.WithContext(ctx)) + } + return http.HandlerFunc(fn) +} + +// GetReqID returns a request ID from the given context if one is present. +// Returns the empty string if a request ID cannot be found. +func GetReqID(ctx context.Context) string { + if ctx == nil { + return "" + } + if reqID, ok := ctx.Value(RequestIDKey).(string); ok { + return reqID + } + return "" +} + +// NextRequestID generates the next request ID in the sequence. +func NextRequestID() uint64 { + return atomic.AddUint64(&reqid, 1) +} diff --git a/internal/middleware/request_id_test.go b/internal/middleware/request_id_test.go new file mode 100644 index 00000000..cf07f185 --- /dev/null +++ b/internal/middleware/request_id_test.go @@ -0,0 +1,71 @@ +package middleware + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" +) + +func maintainDefaultRequestID() func() { + original := RequestIDHeader + + return func() { + RequestIDHeader = original + } +} + +func TestRequestID(t *testing.T) { + tests := map[string]struct { + requestIDHeader string + request func() *http.Request + expectedResponse string + }{ + "Retrieves Request Id from default header": { + "X-Request-Id", + func() *http.Request { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Add("X-Request-Id", "req-123456") + + return req + }, + "RequestID: req-123456", + }, + "Retrieves Request Id from custom header": { + "X-Trace-Id", + func() *http.Request { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Add("X-Trace-Id", "trace:abc123") + + return req + }, + "RequestID: trace:abc123", + }, + } + + defer maintainDefaultRequestID()() + + for _, test := range tests { + w := httptest.NewRecorder() + + r := chi.NewRouter() + + RequestIDHeader = test.requestIDHeader + + r.Use(RequestID) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + requestID := GetReqID(r.Context()) + response := fmt.Sprintf("RequestID: %s", requestID) + + w.Write([]byte(response)) + }) + r.ServeHTTP(w, test.request()) + + if w.Body.String() != test.expectedResponse { + t.Fatalf("RequestID was not the expected value") + } + } +} diff --git a/internal/middleware/terminal.go b/internal/middleware/terminal.go new file mode 100644 index 00000000..b4657747 --- /dev/null +++ b/internal/middleware/terminal.go @@ -0,0 +1,66 @@ +// From go-chi +// https://github.com/go-chi/chi/blob/d32a83448b5f43e42bc96487c6b0b3667a92a2e4/middleware/terminal.go + +package middleware + +// Ported from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware + +import ( + "fmt" + "io" + "os" +) + +var ( + // Normal colors + nBlack = []byte{'\033', '[', '3', '0', 'm'} + nRed = []byte{'\033', '[', '3', '1', 'm'} + nGreen = []byte{'\033', '[', '3', '2', 'm'} + nYellow = []byte{'\033', '[', '3', '3', 'm'} + nBlue = []byte{'\033', '[', '3', '4', 'm'} + nMagenta = []byte{'\033', '[', '3', '5', 'm'} + nCyan = []byte{'\033', '[', '3', '6', 'm'} + nWhite = []byte{'\033', '[', '3', '7', 'm'} + // Bright colors + bBlack = []byte{'\033', '[', '3', '0', ';', '1', 'm'} + bRed = []byte{'\033', '[', '3', '1', ';', '1', 'm'} + bGreen = []byte{'\033', '[', '3', '2', ';', '1', 'm'} + bYellow = []byte{'\033', '[', '3', '3', ';', '1', 'm'} + bBlue = []byte{'\033', '[', '3', '4', ';', '1', 'm'} + bMagenta = []byte{'\033', '[', '3', '5', ';', '1', 'm'} + bCyan = []byte{'\033', '[', '3', '6', ';', '1', 'm'} + bWhite = []byte{'\033', '[', '3', '7', ';', '1', 'm'} + + reset = []byte{'\033', '[', '0', 'm'} +) + +var IsTTY bool + +func init() { + // This is sort of cheating: if stdout is a character device, we assume + // that means it's a TTY. Unfortunately, there are many non-TTY + // character devices, but fortunately stdout is rarely set to any of + // them. + // + // We could solve this properly by pulling in a dependency on + // code.google.com/p/go.crypto/ssh/terminal, for instance, but as a + // heuristic for whether to print in color or in black-and-white, I'd + // really rather not. + fi, err := os.Stdout.Stat() + if err == nil { + m := os.ModeDevice | os.ModeCharDevice + IsTTY = fi.Mode()&m == m + } +} + +// colorWrite +func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) { + if IsTTY && useColor { + w.Write(color) + } + fmt.Fprintf(w, s, args...) + if IsTTY && useColor { + w.Write(reset) + } +} diff --git a/internal/middleware/wrap_writer.go b/internal/middleware/wrap_writer.go new file mode 100644 index 00000000..09be0800 --- /dev/null +++ b/internal/middleware/wrap_writer.go @@ -0,0 +1,221 @@ +// WrapWriter from go-chi +// https://github.com/go-chi/chi/blob/d32a83448b5f43e42bc96487c6b0b3667a92a2e4/middleware/wrap_writer.go +package middleware + +// The original work was derived from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware + +import ( + "bufio" + "io" + "net" + "net/http" +) + +// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to +// hook into various parts of the response process. +func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter { + _, fl := w.(http.Flusher) + + bw := basicWriter{ResponseWriter: w} + + if protoMajor == 2 { + _, ps := w.(http.Pusher) + if fl && ps { + return &http2FancyWriter{bw} + } + } else { + _, hj := w.(http.Hijacker) + _, rf := w.(io.ReaderFrom) + if fl && hj && rf { + return &httpFancyWriter{bw} + } + if fl && hj { + return &flushHijackWriter{bw} + } + if hj { + return &hijackWriter{bw} + } + } + + if fl { + return &flushWriter{bw} + } + + return &bw +} + +// WrapResponseWriter is a proxy around an http.ResponseWriter that allows you to hook +// into various parts of the response process. +type WrapResponseWriter interface { + http.ResponseWriter + // Status returns the HTTP status of the request, or 0 if one has not + // yet been sent. + Status() int + // BytesWritten returns the total number of bytes sent to the client. + BytesWritten() int + // Tee causes the response body to be written to the given io.Writer in + // addition to proxying the writes through. Only one io.Writer can be + // tee'd to at once: setting a second one will overwrite the first. + // Writes will be sent to the proxy before being written to this + // io.Writer. It is illegal for the tee'd writer to be modified + // concurrently with writes. + Tee(io.Writer) + // Unwrap returns the original proxied target. + Unwrap() http.ResponseWriter +} + +// basicWriter wraps a http.ResponseWriter that implements the minimal +// http.ResponseWriter interface. +type basicWriter struct { + http.ResponseWriter + wroteHeader bool + code int + bytes int + tee io.Writer +} + +func (b *basicWriter) WriteHeader(code int) { + if !b.wroteHeader { + b.code = code + b.wroteHeader = true + b.ResponseWriter.WriteHeader(code) + } +} + +func (b *basicWriter) Write(buf []byte) (int, error) { + b.maybeWriteHeader() + n, err := b.ResponseWriter.Write(buf) + if b.tee != nil { + _, err2 := b.tee.Write(buf[:n]) + // Prefer errors generated by the proxied writer. + if err == nil { + err = err2 + } + } + b.bytes += n + return n, err +} + +func (b *basicWriter) maybeWriteHeader() { + if !b.wroteHeader { + b.WriteHeader(http.StatusOK) + } +} + +func (b *basicWriter) Status() int { + return b.code +} + +func (b *basicWriter) BytesWritten() int { + return b.bytes +} + +func (b *basicWriter) Tee(w io.Writer) { + b.tee = w +} + +func (b *basicWriter) Unwrap() http.ResponseWriter { + return b.ResponseWriter +} + +// flushWriter ... +type flushWriter struct { + basicWriter +} + +func (f *flushWriter) Flush() { + f.wroteHeader = true + fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl.Flush() +} + +var _ http.Flusher = &flushWriter{} + +// hijackWriter ... +type hijackWriter struct { + basicWriter +} + +func (f *hijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj := f.basicWriter.ResponseWriter.(http.Hijacker) + return hj.Hijack() +} + +var _ http.Hijacker = &hijackWriter{} + +// flushHijackWriter ... +type flushHijackWriter struct { + basicWriter +} + +func (f *flushHijackWriter) Flush() { + f.wroteHeader = true + fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl.Flush() +} + +func (f *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj := f.basicWriter.ResponseWriter.(http.Hijacker) + return hj.Hijack() +} + +var _ http.Flusher = &flushHijackWriter{} +var _ http.Hijacker = &flushHijackWriter{} + +// httpFancyWriter is a HTTP writer that additionally satisfies +// http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case +// of wrapping the http.ResponseWriter that package http gives you, in order to +// make the proxied object support the full method set of the proxied object. +type httpFancyWriter struct { + basicWriter +} + +func (f *httpFancyWriter) Flush() { + f.wroteHeader = true + fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl.Flush() +} + +func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj := f.basicWriter.ResponseWriter.(http.Hijacker) + return hj.Hijack() +} + +func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error { + return f.basicWriter.ResponseWriter.(http.Pusher).Push(target, opts) +} + +func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) { + if f.basicWriter.tee != nil { + n, err := io.Copy(&f.basicWriter, r) + f.basicWriter.bytes += int(n) + return n, err + } + rf := f.basicWriter.ResponseWriter.(io.ReaderFrom) + f.basicWriter.maybeWriteHeader() + n, err := rf.ReadFrom(r) + f.basicWriter.bytes += int(n) + return n, err +} + +var _ http.Flusher = &httpFancyWriter{} +var _ http.Hijacker = &httpFancyWriter{} +var _ http.Pusher = &http2FancyWriter{} +var _ io.ReaderFrom = &httpFancyWriter{} + +// http2FancyWriter is a HTTP2 writer that additionally satisfies +// http.Flusher, and io.ReaderFrom. It exists for the common case +// of wrapping the http.ResponseWriter that package http gives you, in order to +// make the proxied object support the full method set of the proxied object. +type http2FancyWriter struct { + basicWriter +} + +func (f *http2FancyWriter) Flush() { + f.wroteHeader = true + fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl.Flush() +} + +var _ http.Flusher = &http2FancyWriter{} diff --git a/internal/middleware/wrap_writer_test.go b/internal/middleware/wrap_writer_test.go new file mode 100644 index 00000000..2c442ada --- /dev/null +++ b/internal/middleware/wrap_writer_test.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "net/http/httptest" + "testing" +) + +func TestHttpFancyWriterRemembersWroteHeaderWhenFlushed(t *testing.T) { + f := &httpFancyWriter{basicWriter: basicWriter{ResponseWriter: httptest.NewRecorder()}} + f.Flush() + + if !f.wroteHeader { + t.Fatal("want Flush to have set wroteHeader=true") + } +} + +func TestHttp2FancyWriterRemembersWroteHeaderWhenFlushed(t *testing.T) { + f := &http2FancyWriter{basicWriter{ResponseWriter: httptest.NewRecorder()}} + f.Flush() + + if !f.wroteHeader { + t.Fatal("want Flush to have set wroteHeader=true") + } +} diff --git a/internal/server/auth_callback.go b/internal/server/auth_callback.go new file mode 100644 index 00000000..bdddc0e6 --- /dev/null +++ b/internal/server/auth_callback.go @@ -0,0 +1,47 @@ +package server + +import ( + "context" + "fmt" + "time" + + "github.com/danielgtaylor/huma/v2" +) + +// CallbackInput defines the query parameters for the callback endpoint. +type CallbackInput struct { + Code string `query:"code" validate:"required"` +} + +// CallbackResponse defines the JSON structure for the response. +// Huma will automatically encode this as JSON. +type CallbackResponse struct { + Body struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + Expiry time.Time `json:"expiry"` + } +} + +// Callback handles the OAuth2 callback. +func (s *Server) Callback(ctx context.Context, in *CallbackInput) (*CallbackResponse, error) { + // Validate the code parameter. + if in.Code == "" { + return nil, huma.Error400BadRequest("No code provided") + } + + // Exchange the code for tokens. + oauth2Token, err := s.OauthConfig.Exchange(ctx, in.Code) + if err != nil { + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange token: %v", err)) + } + + // For development, return the token details as JSON. + cbResponse := &CallbackResponse{} + cbResponse.Body.AccessToken = oauth2Token.AccessToken + cbResponse.Body.TokenType = oauth2Token.TokenType + cbResponse.Body.RefreshToken = oauth2Token.RefreshToken + cbResponse.Body.Expiry = oauth2Token.Expiry + return cbResponse, nil +} diff --git a/internal/server/login.go b/internal/server/login.go new file mode 100644 index 00000000..db084bb7 --- /dev/null +++ b/internal/server/login.go @@ -0,0 +1,43 @@ +package server + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" + "golang.org/x/oauth2" +) + +type OauthLoginResponse struct { + Status int + Url string `header:"Location"` + Cookie string `header:"Set-Cookie"` +} + +// Login handles the OAuth login redirect. +func (s *Server) Login(ctx context.Context, _ *EmptyInput) (*OauthLoginResponse, error) { + // Generate a random state value for CSRF protection. + state := uuid.New().String() + + // Build the OAuth2 authentication URL with the state. + authURL := s.OauthConfig.AuthCodeURL(state, oauth2.AccessTypeOnline) + + // Create a cookie that stores the state value. + cookie := &http.Cookie{ + Name: "state", + Value: state, + Path: "/", + MaxAge: int(time.Hour.Seconds()), + Secure: false, // Change to true if using HTTPS in production. + HttpOnly: true, + } + + // Instead of calling http.Redirect, we return a response struct with the headers. + // Huma will set the "Location" and "Set-Cookie" headers accordingly. + return &OauthLoginResponse{ + Status: http.StatusTemporaryRedirect, + Url: authURL, + Cookie: cookie.String(), + }, nil +} diff --git a/internal/server/server.go b/internal/server/server.go index 72557a47..dd5515f5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,12 +3,19 @@ package server import ( "context" + "github.com/unbindapp/unbind-api/config" "github.com/unbindapp/unbind-api/internal/kubeclient" + "golang.org/x/oauth2" ) +// EmptyInput can be used when no input is needed. +type EmptyInput struct{} + // Server implements generated.ServerInterface type Server struct { - KubeClient *kubeclient.KubeClient + KubeClient *kubeclient.KubeClient + Cfg *config.Config + OauthConfig *oauth2.Config } // HealthCheck is your /health endpoint @@ -18,7 +25,7 @@ type HealthResponse struct { } } -func (s *Server) HealthCheck(ctx context.Context, _ *struct{}) (*HealthResponse, error) { +func (s *Server) HealthCheck(ctx context.Context, _ *EmptyInput) (*HealthResponse, error) { healthResponse := &HealthResponse{} healthResponse.Body.Status = "ok" return healthResponse, nil diff --git a/internal/server/teams.go b/internal/server/teams.go index f10bb0e3..d36b4925 100644 --- a/internal/server/teams.go +++ b/internal/server/teams.go @@ -15,7 +15,7 @@ type TeamResponse struct { } // ListTeams handles GET /teams -func (s *Server) ListTeams(ctx context.Context, _ *struct{}) (*TeamResponse, error) { +func (s *Server) ListTeams(ctx context.Context, _ *EmptyInput) (*TeamResponse, error) { teams, err := s.KubeClient.GetUnbindTeams() if err != nil { log.Error("Error getting teams", "err", err) From 6c03447324d85e3a17d38130081dddde0aaa94bf Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 17 Feb 2025 22:23:55 +0000 Subject: [PATCH 06/22] E2E login flow --- .devcontainer/dex.yaml | 6 ++++++ .devcontainer/docker-compose.yaml | 1 + cmd/main.go | 9 +++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.devcontainer/dex.yaml b/.devcontainer/dex.yaml index e5feea1a..de9407ac 100644 --- a/.devcontainer/dex.yaml +++ b/.devcontainer/dex.yaml @@ -20,6 +20,12 @@ staticPasswords: oauth2: skipApprovalScreen: true + responseTypes: + - token + - code + - id_token + # Add this to explicitly enable offline access + alwaysIssueOfflineToken: t staticClients: - id: unbind-dev diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index 005b78ac..1ceca01f 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -13,6 +13,7 @@ services: - PGDATA=/var/lib/postgresql/data/dev volumes: - ../.data/postgres:/var/lib/postgresql/data:delegated + - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql networks: - app-network diff --git a/cmd/main.go b/cmd/main.go index d93821ed..9847a09b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,9 @@ package main import ( + "context" "encoding/json" "fmt" - "log" "net/http" "github.com/danielgtaylor/huma/v2" @@ -15,6 +15,7 @@ import ( "github.com/unbindapp/unbind-api/internal/database" "github.com/unbindapp/unbind-api/internal/database/repository" "github.com/unbindapp/unbind-api/internal/kubeclient" + "github.com/unbindapp/unbind-api/internal/log" "github.com/unbindapp/unbind-api/internal/middleware" "github.com/unbindapp/unbind-api/internal/server" "golang.org/x/oauth2" @@ -36,6 +37,10 @@ func main() { if err != nil { log.Fatalf("Failed to create ent client: %v", err) } + log.Info("🦋 Running migrations...") + if err := db.Schema.Create(context.TODO()); err != nil { + log.Fatal("Failed to run migrations", "err", err) + } repo := repository.NewRepository(db) // Create kubernetes client @@ -55,7 +60,7 @@ func main() { }, // ! TODO - adjust redirect when necessary RedirectURL: "http://localhost:8089/auth/callback", - Scopes: []string{"openid", "profile", "email"}, + Scopes: []string{"openid", "profile", "email", "offline_access"}, }, } From b39951d3fd119e3de8684ba2a241bc0500712420 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 25 Feb 2025 16:34:21 +0000 Subject: [PATCH 07/22] Github app manifest flow --- cmd/main.go | 45 +- config/config.go | 27 + ent/client.go | 161 +++- ent/ent.go | 4 +- ent/githubapp.go | 184 +++++ ent/githubapp/githubapp.go | 119 +++ ent/githubapp/where.go | 556 +++++++++++++ ent/githubapp_create.go | 895 +++++++++++++++++++++ ent/githubapp_delete.go | 88 ++ ent/githubapp_query.go | 551 +++++++++++++ ent/githubapp_update.go | 478 +++++++++++ ent/hook/hook.go | 12 + ent/migrate/schema.go | 33 + ent/mutation.go | 750 ++++++++++++++++- ent/predicate/predicate.go | 3 + ent/runtime.go | 26 + ent/schema/github_app.go | 68 ++ ent/schema/user.go | 11 + ent/tx.go | 5 +- go.mod | 4 +- go.sum | 13 + internal/database/repository/github_app.go | 23 + internal/github/github.go | 43 + internal/github/manifest.go | 69 ++ internal/middleware/auth.go | 65 +- internal/middleware/logger.go | 2 +- internal/middleware/middleware.go | 5 +- internal/server/github.go | 80 ++ internal/server/server.go | 10 +- internal/server/user.go | 29 + internal/utils/url.go | 52 ++ internal/utils/url_test.go | 194 +++++ 32 files changed, 4533 insertions(+), 72 deletions(-) create mode 100644 ent/githubapp.go create mode 100644 ent/githubapp/githubapp.go create mode 100644 ent/githubapp/where.go create mode 100644 ent/githubapp_create.go create mode 100644 ent/githubapp_delete.go create mode 100644 ent/githubapp_query.go create mode 100644 ent/githubapp_update.go create mode 100644 ent/schema/github_app.go create mode 100644 internal/database/repository/github_app.go create mode 100644 internal/github/github.go create mode 100644 internal/github/manifest.go create mode 100644 internal/server/github.go create mode 100644 internal/server/user.go create mode 100644 internal/utils/url.go create mode 100644 internal/utils/url_test.go diff --git a/cmd/main.go b/cmd/main.go index 9847a09b..0b4ef86f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "fmt" "net/http" @@ -11,9 +10,9 @@ import ( "github.com/go-chi/chi/v5" "github.com/joho/godotenv" "github.com/unbindapp/unbind-api/config" - "github.com/unbindapp/unbind-api/ent" "github.com/unbindapp/unbind-api/internal/database" "github.com/unbindapp/unbind-api/internal/database/repository" + "github.com/unbindapp/unbind-api/internal/github" "github.com/unbindapp/unbind-api/internal/kubeclient" "github.com/unbindapp/unbind-api/internal/log" "github.com/unbindapp/unbind-api/internal/middleware" @@ -50,6 +49,7 @@ func main() { srvImpl := &server.Server{ KubeClient: kubeClient, Cfg: cfg, + Repository: repo, // Create an OAuth2 configuration using the Dex OauthConfig: &oauth2.Config{ ClientID: cfg.DexClientID, @@ -62,37 +62,40 @@ func main() { RedirectURL: "http://localhost:8089/auth/callback", Scopes: []string{"openid", "profile", "email", "offline_access"}, }, + GithubClient: github.NewGithubClient(cfg), } + // New chi router + r := chi.NewRouter() + r.Use(middleware.Logger) + api := humachi.New(r, huma.DefaultConfig("Unbind API", "1.0.0")) + // Create middleware - mw, err := middleware.NewMiddleware(cfg, repo) + mw, err := middleware.NewMiddleware(cfg, repo, api) if err != nil { log.Fatalf("Failed to create middleware: %v", err) } - // New chi router - r := chi.NewRouter() - r.Use(mw.Logger) - api := humachi.New(r, huma.DefaultConfig("Unbind API", "1.0.0")) - // Add routes huma.Get(api, "/healthz", srvImpl.HealthCheck) - r.Group(func(r chi.Router) { - huma.Get(api, "/auth/login", srvImpl.Login) - huma.Get(api, "/auth/callback", srvImpl.Callback) - }) + // /auth group + authGrp := huma.NewGroup(api, "/auth") + huma.Get(authGrp, "/login", srvImpl.Login) + huma.Get(authGrp, "/callback", srvImpl.Callback) + + // /user group + userGrp := huma.NewGroup(api, "/user") + userGrp.UseMiddleware(mw.Authenticate) + huma.Get(userGrp, "/me", srvImpl.Me) - // Protected routes - r.Group(func(r chi.Router) { - r.Use(mw.Authenticate) + ghGroup := huma.NewGroup(api, "/github") + ghGroup.UseMiddleware(mw.Authenticate) + huma.Post(ghGroup, "/app/manifest", srvImpl.GithubManifestCreate) + huma.Post(ghGroup, "/app/connect", srvImpl.GithubAppConnect) - r.Get("/api/me", func(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value("user").(*ent.User) - json.NewEncoder(w).Encode(user) - }) - }) - huma.Get(api, "/teams", srvImpl.ListTeams) + // ! + // huma.Get(api, "/teams", srvImpl.ListTeams) // Start the server addr := ":8089" diff --git a/config/config.go b/config/config.go index 71588f05..85c436a3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,11 +1,23 @@ package config import ( + "net/url" + "path" + "strings" + "github.com/caarlos0/env/v11" "github.com/unbindapp/unbind-api/internal/log" + "github.com/unbindapp/unbind-api/internal/utils" ) type Config struct { + // Root + ExternalURL string `env:"EXTERNAL_URL" envDefault:"http://localhost:8089"` + // Github Specific + GithubURL string `env:"GITHUB_URL" envDefault:"https://github.com"` // Override for github enterprise + GithubWebhookURL string + // By default we will just use the external URL for the bind and unbind suffixes + UnbindSuffix string `env:"UNBIND_SUFFIX"` // Postgres PostgresHost string `env:"POSTGRES_HOST" envDefault:"localhost"` PostgresPort int `env:"POSTGRES_PORT" envDefault:"5432"` @@ -27,5 +39,20 @@ func NewConfig() *Config { if err := env.Parse(&cfg); err != nil { log.Fatal("Error parsing environment", "err", err) } + + // Get suffix if not present + if cfg.UnbindSuffix == "" { + suffix, err := utils.ValidateAndExtractDomain(cfg.ExternalURL) + if err != nil { + log.Fatal("Error extracting domain from external URL", "err", err) + } + cfg.UnbindSuffix = strings.ToLower(suffix) + } + + // Parse github callback URL + baseURL, _ := url.Parse(cfg.ExternalURL) + baseURL.Path = path.Join(baseURL.Path, "webhook/github") + cfg.GithubWebhookURL = baseURL.String() + return &cfg } diff --git a/ent/client.go b/ent/client.go index e042ca59..1b977a7f 100644 --- a/ent/client.go +++ b/ent/client.go @@ -15,6 +15,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" + "github.com/unbindapp/unbind-api/ent/githubapp" "github.com/unbindapp/unbind-api/ent/user" stdsql "database/sql" @@ -25,6 +26,8 @@ type Client struct { config // Schema is the client for creating, migrating and dropping schema. Schema *migrate.Schema + // GithubApp is the client for interacting with the GithubApp builders. + GithubApp *GithubAppClient // User is the client for interacting with the User builders. User *UserClient } @@ -38,6 +41,7 @@ func NewClient(opts ...Option) *Client { func (c *Client) init() { c.Schema = migrate.NewSchema(c.driver) + c.GithubApp = NewGithubAppClient(c.config) c.User = NewUserClient(c.config) } @@ -129,9 +133,10 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { cfg := c.config cfg.driver = tx return &Tx{ - ctx: ctx, - config: cfg, - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + GithubApp: NewGithubAppClient(cfg), + User: NewUserClient(cfg), }, nil } @@ -149,16 +154,17 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) cfg := c.config cfg.driver = &txDriver{tx: tx, drv: c.driver} return &Tx{ - ctx: ctx, - config: cfg, - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + GithubApp: NewGithubAppClient(cfg), + User: NewUserClient(cfg), }, nil } // Debug returns a new debug-client. It's used to get verbose logging on specific operations. // // client.Debug(). -// User. +// GithubApp. // Query(). // Count(ctx) func (c *Client) Debug() *Client { @@ -180,18 +186,22 @@ func (c *Client) Close() error { // Use adds the mutation hooks to all the entity clients. // In order to add hooks to a specific client, call: `client.Node.Use(...)`. func (c *Client) Use(hooks ...Hook) { + c.GithubApp.Use(hooks...) c.User.Use(hooks...) } // Intercept adds the query interceptors to all the entity clients. // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. func (c *Client) Intercept(interceptors ...Interceptor) { + c.GithubApp.Intercept(interceptors...) c.User.Intercept(interceptors...) } // Mutate implements the ent.Mutator interface. func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { switch m := m.(type) { + case *GithubAppMutation: + return c.GithubApp.mutate(ctx, m) case *UserMutation: return c.User.mutate(ctx, m) default: @@ -199,6 +209,139 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { } } +// GithubAppClient is a client for the GithubApp schema. +type GithubAppClient struct { + config +} + +// NewGithubAppClient returns a client for the GithubApp from the given config. +func NewGithubAppClient(c config) *GithubAppClient { + return &GithubAppClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `githubapp.Hooks(f(g(h())))`. +func (c *GithubAppClient) Use(hooks ...Hook) { + c.hooks.GithubApp = append(c.hooks.GithubApp, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `githubapp.Intercept(f(g(h())))`. +func (c *GithubAppClient) Intercept(interceptors ...Interceptor) { + c.inters.GithubApp = append(c.inters.GithubApp, interceptors...) +} + +// Create returns a builder for creating a GithubApp entity. +func (c *GithubAppClient) Create() *GithubAppCreate { + mutation := newGithubAppMutation(c.config, OpCreate) + return &GithubAppCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of GithubApp entities. +func (c *GithubAppClient) CreateBulk(builders ...*GithubAppCreate) *GithubAppCreateBulk { + return &GithubAppCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GithubAppClient) MapCreateBulk(slice any, setFunc func(*GithubAppCreate, int)) *GithubAppCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GithubAppCreateBulk{err: fmt.Errorf("calling to GithubAppClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GithubAppCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GithubAppCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for GithubApp. +func (c *GithubAppClient) Update() *GithubAppUpdate { + mutation := newGithubAppMutation(c.config, OpUpdate) + return &GithubAppUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GithubAppClient) UpdateOne(ga *GithubApp) *GithubAppUpdateOne { + mutation := newGithubAppMutation(c.config, OpUpdateOne, withGithubApp(ga)) + return &GithubAppUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GithubAppClient) UpdateOneID(id uuid.UUID) *GithubAppUpdateOne { + mutation := newGithubAppMutation(c.config, OpUpdateOne, withGithubAppID(id)) + return &GithubAppUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for GithubApp. +func (c *GithubAppClient) Delete() *GithubAppDelete { + mutation := newGithubAppMutation(c.config, OpDelete) + return &GithubAppDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GithubAppClient) DeleteOne(ga *GithubApp) *GithubAppDeleteOne { + return c.DeleteOneID(ga.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GithubAppClient) DeleteOneID(id uuid.UUID) *GithubAppDeleteOne { + builder := c.Delete().Where(githubapp.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GithubAppDeleteOne{builder} +} + +// Query returns a query builder for GithubApp. +func (c *GithubAppClient) Query() *GithubAppQuery { + return &GithubAppQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGithubApp}, + inters: c.Interceptors(), + } +} + +// Get returns a GithubApp entity by its id. +func (c *GithubAppClient) Get(ctx context.Context, id uuid.UUID) (*GithubApp, error) { + return c.Query().Where(githubapp.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GithubAppClient) GetX(ctx context.Context, id uuid.UUID) *GithubApp { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *GithubAppClient) Hooks() []Hook { + return c.hooks.GithubApp +} + +// Interceptors returns the client interceptors. +func (c *GithubAppClient) Interceptors() []Interceptor { + return c.inters.GithubApp +} + +func (c *GithubAppClient) mutate(ctx context.Context, m *GithubAppMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GithubAppCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GithubAppUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GithubAppUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GithubAppDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown GithubApp mutation op: %q", m.Op()) + } +} + // UserClient is a client for the User schema. type UserClient struct { config @@ -335,10 +478,10 @@ func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) // hooks and interceptors per client, for fast access. type ( hooks struct { - User []ent.Hook + GithubApp, User []ent.Hook } inters struct { - User []ent.Interceptor + GithubApp, User []ent.Interceptor } ) diff --git a/ent/ent.go b/ent/ent.go index 7a46d066..9728dda3 100644 --- a/ent/ent.go +++ b/ent/ent.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/unbindapp/unbind-api/ent/githubapp" "github.com/unbindapp/unbind-api/ent/user" ) @@ -73,7 +74,8 @@ var ( func checkColumn(table, column string) error { initCheck.Do(func() { columnCheck = sql.NewColumnCheck(map[string]func(string) bool{ - user.Table: user.ValidColumn, + githubapp.Table: githubapp.ValidColumn, + user.Table: user.ValidColumn, }) }) return columnCheck(table, column) diff --git a/ent/githubapp.go b/ent/githubapp.go new file mode 100644 index 00000000..8b201009 --- /dev/null +++ b/ent/githubapp.go @@ -0,0 +1,184 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/githubapp" +) + +// GithubApp is the model entity for the GithubApp schema. +type GithubApp struct { + config `json:"-"` + // ID of the ent. + // The primary key of the entity. + ID uuid.UUID `json:"id"` + // The time at which the entity was created. + CreatedAt time.Time `json:"created_at,omitempty"` + // The time at which the entity was last updated. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // The GitHub App ID + GithubAppID int64 `json:"github_app_id,omitempty"` + // Name of the GitHub App + Name string `json:"name,omitempty"` + // OAuth client ID of the GitHub App + ClientID string `json:"client_id,omitempty"` + // OAuth client secret of the GitHub App + ClientSecret string `json:"-"` + // Webhook secret for GitHub events + WebhookSecret string `json:"-"` + // Private key (PEM) for GitHub App authentication + PrivateKey string `json:"-"` + selectValues sql.SelectValues +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*GithubApp) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case githubapp.FieldGithubAppID: + values[i] = new(sql.NullInt64) + case githubapp.FieldName, githubapp.FieldClientID, githubapp.FieldClientSecret, githubapp.FieldWebhookSecret, githubapp.FieldPrivateKey: + values[i] = new(sql.NullString) + case githubapp.FieldCreatedAt, githubapp.FieldUpdatedAt: + values[i] = new(sql.NullTime) + case githubapp.FieldID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the GithubApp fields. +func (ga *GithubApp) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case githubapp.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + ga.ID = *value + } + case githubapp.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + ga.CreatedAt = value.Time + } + case githubapp.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + ga.UpdatedAt = value.Time + } + case githubapp.FieldGithubAppID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field github_app_id", values[i]) + } else if value.Valid { + ga.GithubAppID = value.Int64 + } + case githubapp.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + ga.Name = value.String + } + case githubapp.FieldClientID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field client_id", values[i]) + } else if value.Valid { + ga.ClientID = value.String + } + case githubapp.FieldClientSecret: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field client_secret", values[i]) + } else if value.Valid { + ga.ClientSecret = value.String + } + case githubapp.FieldWebhookSecret: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field webhook_secret", values[i]) + } else if value.Valid { + ga.WebhookSecret = value.String + } + case githubapp.FieldPrivateKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field private_key", values[i]) + } else if value.Valid { + ga.PrivateKey = value.String + } + default: + ga.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the GithubApp. +// This includes values selected through modifiers, order, etc. +func (ga *GithubApp) Value(name string) (ent.Value, error) { + return ga.selectValues.Get(name) +} + +// Update returns a builder for updating this GithubApp. +// Note that you need to call GithubApp.Unwrap() before calling this method if this GithubApp +// was returned from a transaction, and the transaction was committed or rolled back. +func (ga *GithubApp) Update() *GithubAppUpdateOne { + return NewGithubAppClient(ga.config).UpdateOne(ga) +} + +// Unwrap unwraps the GithubApp entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (ga *GithubApp) Unwrap() *GithubApp { + _tx, ok := ga.config.driver.(*txDriver) + if !ok { + panic("ent: GithubApp is not a transactional entity") + } + ga.config.driver = _tx.drv + return ga +} + +// String implements the fmt.Stringer. +func (ga *GithubApp) String() string { + var builder strings.Builder + builder.WriteString("GithubApp(") + builder.WriteString(fmt.Sprintf("id=%v, ", ga.ID)) + builder.WriteString("created_at=") + builder.WriteString(ga.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(ga.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("github_app_id=") + builder.WriteString(fmt.Sprintf("%v", ga.GithubAppID)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(ga.Name) + builder.WriteString(", ") + builder.WriteString("client_id=") + builder.WriteString(ga.ClientID) + builder.WriteString(", ") + builder.WriteString("client_secret=") + builder.WriteString(", ") + builder.WriteString("webhook_secret=") + builder.WriteString(", ") + builder.WriteString("private_key=") + builder.WriteByte(')') + return builder.String() +} + +// GithubApps is a parsable slice of GithubApp. +type GithubApps []*GithubApp diff --git a/ent/githubapp/githubapp.go b/ent/githubapp/githubapp.go new file mode 100644 index 00000000..bfa06bd8 --- /dev/null +++ b/ent/githubapp/githubapp.go @@ -0,0 +1,119 @@ +// Code generated by ent, DO NOT EDIT. + +package githubapp + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the githubapp type in the database. + Label = "github_app" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldGithubAppID holds the string denoting the github_app_id field in the database. + FieldGithubAppID = "github_app_id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldClientID holds the string denoting the client_id field in the database. + FieldClientID = "client_id" + // FieldClientSecret holds the string denoting the client_secret field in the database. + FieldClientSecret = "client_secret" + // FieldWebhookSecret holds the string denoting the webhook_secret field in the database. + FieldWebhookSecret = "webhook_secret" + // FieldPrivateKey holds the string denoting the private_key field in the database. + FieldPrivateKey = "private_key" + // Table holds the table name of the githubapp in the database. + Table = "github_apps" +) + +// Columns holds all SQL columns for githubapp fields. +var Columns = []string{ + FieldID, + FieldCreatedAt, + FieldUpdatedAt, + FieldGithubAppID, + FieldName, + FieldClientID, + FieldClientSecret, + FieldWebhookSecret, + FieldPrivateKey, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// OrderOption defines the ordering options for the GithubApp queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByGithubAppID orders the results by the github_app_id field. +func ByGithubAppID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGithubAppID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByClientID orders the results by the client_id field. +func ByClientID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldClientID, opts...).ToFunc() +} + +// ByClientSecret orders the results by the client_secret field. +func ByClientSecret(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldClientSecret, opts...).ToFunc() +} + +// ByWebhookSecret orders the results by the webhook_secret field. +func ByWebhookSecret(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldWebhookSecret, opts...).ToFunc() +} + +// ByPrivateKey orders the results by the private_key field. +func ByPrivateKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPrivateKey, opts...).ToFunc() +} diff --git a/ent/githubapp/where.go b/ent/githubapp/where.go new file mode 100644 index 00000000..050c781a --- /dev/null +++ b/ent/githubapp/where.go @@ -0,0 +1,556 @@ +// Code generated by ent, DO NOT EDIT. + +package githubapp + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldID, id)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// GithubAppID applies equality check predicate on the "github_app_id" field. It's identical to GithubAppIDEQ. +func GithubAppID(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldGithubAppID, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldName, v)) +} + +// ClientID applies equality check predicate on the "client_id" field. It's identical to ClientIDEQ. +func ClientID(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldClientID, v)) +} + +// ClientSecret applies equality check predicate on the "client_secret" field. It's identical to ClientSecretEQ. +func ClientSecret(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldClientSecret, v)) +} + +// WebhookSecret applies equality check predicate on the "webhook_secret" field. It's identical to WebhookSecretEQ. +func WebhookSecret(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldWebhookSecret, v)) +} + +// PrivateKey applies equality check predicate on the "private_key" field. It's identical to PrivateKeyEQ. +func PrivateKey(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldPrivateKey, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// GithubAppIDEQ applies the EQ predicate on the "github_app_id" field. +func GithubAppIDEQ(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldGithubAppID, v)) +} + +// GithubAppIDNEQ applies the NEQ predicate on the "github_app_id" field. +func GithubAppIDNEQ(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldGithubAppID, v)) +} + +// GithubAppIDIn applies the In predicate on the "github_app_id" field. +func GithubAppIDIn(vs ...int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldGithubAppID, vs...)) +} + +// GithubAppIDNotIn applies the NotIn predicate on the "github_app_id" field. +func GithubAppIDNotIn(vs ...int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldGithubAppID, vs...)) +} + +// GithubAppIDGT applies the GT predicate on the "github_app_id" field. +func GithubAppIDGT(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldGithubAppID, v)) +} + +// GithubAppIDGTE applies the GTE predicate on the "github_app_id" field. +func GithubAppIDGTE(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldGithubAppID, v)) +} + +// GithubAppIDLT applies the LT predicate on the "github_app_id" field. +func GithubAppIDLT(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldGithubAppID, v)) +} + +// GithubAppIDLTE applies the LTE predicate on the "github_app_id" field. +func GithubAppIDLTE(v int64) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldGithubAppID, v)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContainsFold(FieldName, v)) +} + +// ClientIDEQ applies the EQ predicate on the "client_id" field. +func ClientIDEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldClientID, v)) +} + +// ClientIDNEQ applies the NEQ predicate on the "client_id" field. +func ClientIDNEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldClientID, v)) +} + +// ClientIDIn applies the In predicate on the "client_id" field. +func ClientIDIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldClientID, vs...)) +} + +// ClientIDNotIn applies the NotIn predicate on the "client_id" field. +func ClientIDNotIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldClientID, vs...)) +} + +// ClientIDGT applies the GT predicate on the "client_id" field. +func ClientIDGT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldClientID, v)) +} + +// ClientIDGTE applies the GTE predicate on the "client_id" field. +func ClientIDGTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldClientID, v)) +} + +// ClientIDLT applies the LT predicate on the "client_id" field. +func ClientIDLT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldClientID, v)) +} + +// ClientIDLTE applies the LTE predicate on the "client_id" field. +func ClientIDLTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldClientID, v)) +} + +// ClientIDContains applies the Contains predicate on the "client_id" field. +func ClientIDContains(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContains(FieldClientID, v)) +} + +// ClientIDHasPrefix applies the HasPrefix predicate on the "client_id" field. +func ClientIDHasPrefix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasPrefix(FieldClientID, v)) +} + +// ClientIDHasSuffix applies the HasSuffix predicate on the "client_id" field. +func ClientIDHasSuffix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasSuffix(FieldClientID, v)) +} + +// ClientIDEqualFold applies the EqualFold predicate on the "client_id" field. +func ClientIDEqualFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEqualFold(FieldClientID, v)) +} + +// ClientIDContainsFold applies the ContainsFold predicate on the "client_id" field. +func ClientIDContainsFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContainsFold(FieldClientID, v)) +} + +// ClientSecretEQ applies the EQ predicate on the "client_secret" field. +func ClientSecretEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldClientSecret, v)) +} + +// ClientSecretNEQ applies the NEQ predicate on the "client_secret" field. +func ClientSecretNEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldClientSecret, v)) +} + +// ClientSecretIn applies the In predicate on the "client_secret" field. +func ClientSecretIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldClientSecret, vs...)) +} + +// ClientSecretNotIn applies the NotIn predicate on the "client_secret" field. +func ClientSecretNotIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldClientSecret, vs...)) +} + +// ClientSecretGT applies the GT predicate on the "client_secret" field. +func ClientSecretGT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldClientSecret, v)) +} + +// ClientSecretGTE applies the GTE predicate on the "client_secret" field. +func ClientSecretGTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldClientSecret, v)) +} + +// ClientSecretLT applies the LT predicate on the "client_secret" field. +func ClientSecretLT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldClientSecret, v)) +} + +// ClientSecretLTE applies the LTE predicate on the "client_secret" field. +func ClientSecretLTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldClientSecret, v)) +} + +// ClientSecretContains applies the Contains predicate on the "client_secret" field. +func ClientSecretContains(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContains(FieldClientSecret, v)) +} + +// ClientSecretHasPrefix applies the HasPrefix predicate on the "client_secret" field. +func ClientSecretHasPrefix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasPrefix(FieldClientSecret, v)) +} + +// ClientSecretHasSuffix applies the HasSuffix predicate on the "client_secret" field. +func ClientSecretHasSuffix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasSuffix(FieldClientSecret, v)) +} + +// ClientSecretEqualFold applies the EqualFold predicate on the "client_secret" field. +func ClientSecretEqualFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEqualFold(FieldClientSecret, v)) +} + +// ClientSecretContainsFold applies the ContainsFold predicate on the "client_secret" field. +func ClientSecretContainsFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContainsFold(FieldClientSecret, v)) +} + +// WebhookSecretEQ applies the EQ predicate on the "webhook_secret" field. +func WebhookSecretEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldWebhookSecret, v)) +} + +// WebhookSecretNEQ applies the NEQ predicate on the "webhook_secret" field. +func WebhookSecretNEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldWebhookSecret, v)) +} + +// WebhookSecretIn applies the In predicate on the "webhook_secret" field. +func WebhookSecretIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldWebhookSecret, vs...)) +} + +// WebhookSecretNotIn applies the NotIn predicate on the "webhook_secret" field. +func WebhookSecretNotIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldWebhookSecret, vs...)) +} + +// WebhookSecretGT applies the GT predicate on the "webhook_secret" field. +func WebhookSecretGT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldWebhookSecret, v)) +} + +// WebhookSecretGTE applies the GTE predicate on the "webhook_secret" field. +func WebhookSecretGTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldWebhookSecret, v)) +} + +// WebhookSecretLT applies the LT predicate on the "webhook_secret" field. +func WebhookSecretLT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldWebhookSecret, v)) +} + +// WebhookSecretLTE applies the LTE predicate on the "webhook_secret" field. +func WebhookSecretLTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldWebhookSecret, v)) +} + +// WebhookSecretContains applies the Contains predicate on the "webhook_secret" field. +func WebhookSecretContains(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContains(FieldWebhookSecret, v)) +} + +// WebhookSecretHasPrefix applies the HasPrefix predicate on the "webhook_secret" field. +func WebhookSecretHasPrefix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasPrefix(FieldWebhookSecret, v)) +} + +// WebhookSecretHasSuffix applies the HasSuffix predicate on the "webhook_secret" field. +func WebhookSecretHasSuffix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasSuffix(FieldWebhookSecret, v)) +} + +// WebhookSecretEqualFold applies the EqualFold predicate on the "webhook_secret" field. +func WebhookSecretEqualFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEqualFold(FieldWebhookSecret, v)) +} + +// WebhookSecretContainsFold applies the ContainsFold predicate on the "webhook_secret" field. +func WebhookSecretContainsFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContainsFold(FieldWebhookSecret, v)) +} + +// PrivateKeyEQ applies the EQ predicate on the "private_key" field. +func PrivateKeyEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEQ(FieldPrivateKey, v)) +} + +// PrivateKeyNEQ applies the NEQ predicate on the "private_key" field. +func PrivateKeyNEQ(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNEQ(FieldPrivateKey, v)) +} + +// PrivateKeyIn applies the In predicate on the "private_key" field. +func PrivateKeyIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldIn(FieldPrivateKey, vs...)) +} + +// PrivateKeyNotIn applies the NotIn predicate on the "private_key" field. +func PrivateKeyNotIn(vs ...string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldNotIn(FieldPrivateKey, vs...)) +} + +// PrivateKeyGT applies the GT predicate on the "private_key" field. +func PrivateKeyGT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGT(FieldPrivateKey, v)) +} + +// PrivateKeyGTE applies the GTE predicate on the "private_key" field. +func PrivateKeyGTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldGTE(FieldPrivateKey, v)) +} + +// PrivateKeyLT applies the LT predicate on the "private_key" field. +func PrivateKeyLT(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLT(FieldPrivateKey, v)) +} + +// PrivateKeyLTE applies the LTE predicate on the "private_key" field. +func PrivateKeyLTE(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldLTE(FieldPrivateKey, v)) +} + +// PrivateKeyContains applies the Contains predicate on the "private_key" field. +func PrivateKeyContains(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContains(FieldPrivateKey, v)) +} + +// PrivateKeyHasPrefix applies the HasPrefix predicate on the "private_key" field. +func PrivateKeyHasPrefix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasPrefix(FieldPrivateKey, v)) +} + +// PrivateKeyHasSuffix applies the HasSuffix predicate on the "private_key" field. +func PrivateKeyHasSuffix(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldHasSuffix(FieldPrivateKey, v)) +} + +// PrivateKeyEqualFold applies the EqualFold predicate on the "private_key" field. +func PrivateKeyEqualFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldEqualFold(FieldPrivateKey, v)) +} + +// PrivateKeyContainsFold applies the ContainsFold predicate on the "private_key" field. +func PrivateKeyContainsFold(v string) predicate.GithubApp { + return predicate.GithubApp(sql.FieldContainsFold(FieldPrivateKey, v)) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.GithubApp) predicate.GithubApp { + return predicate.GithubApp(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.GithubApp) predicate.GithubApp { + return predicate.GithubApp(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.GithubApp) predicate.GithubApp { + return predicate.GithubApp(sql.NotPredicates(p)) +} diff --git a/ent/githubapp_create.go b/ent/githubapp_create.go new file mode 100644 index 00000000..dda4e42f --- /dev/null +++ b/ent/githubapp_create.go @@ -0,0 +1,895 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/githubapp" +) + +// GithubAppCreate is the builder for creating a GithubApp entity. +type GithubAppCreate struct { + config + mutation *GithubAppMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetCreatedAt sets the "created_at" field. +func (gac *GithubAppCreate) SetCreatedAt(t time.Time) *GithubAppCreate { + gac.mutation.SetCreatedAt(t) + return gac +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (gac *GithubAppCreate) SetNillableCreatedAt(t *time.Time) *GithubAppCreate { + if t != nil { + gac.SetCreatedAt(*t) + } + return gac +} + +// SetUpdatedAt sets the "updated_at" field. +func (gac *GithubAppCreate) SetUpdatedAt(t time.Time) *GithubAppCreate { + gac.mutation.SetUpdatedAt(t) + return gac +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (gac *GithubAppCreate) SetNillableUpdatedAt(t *time.Time) *GithubAppCreate { + if t != nil { + gac.SetUpdatedAt(*t) + } + return gac +} + +// SetGithubAppID sets the "github_app_id" field. +func (gac *GithubAppCreate) SetGithubAppID(i int64) *GithubAppCreate { + gac.mutation.SetGithubAppID(i) + return gac +} + +// SetName sets the "name" field. +func (gac *GithubAppCreate) SetName(s string) *GithubAppCreate { + gac.mutation.SetName(s) + return gac +} + +// SetClientID sets the "client_id" field. +func (gac *GithubAppCreate) SetClientID(s string) *GithubAppCreate { + gac.mutation.SetClientID(s) + return gac +} + +// SetClientSecret sets the "client_secret" field. +func (gac *GithubAppCreate) SetClientSecret(s string) *GithubAppCreate { + gac.mutation.SetClientSecret(s) + return gac +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (gac *GithubAppCreate) SetWebhookSecret(s string) *GithubAppCreate { + gac.mutation.SetWebhookSecret(s) + return gac +} + +// SetPrivateKey sets the "private_key" field. +func (gac *GithubAppCreate) SetPrivateKey(s string) *GithubAppCreate { + gac.mutation.SetPrivateKey(s) + return gac +} + +// SetID sets the "id" field. +func (gac *GithubAppCreate) SetID(u uuid.UUID) *GithubAppCreate { + gac.mutation.SetID(u) + return gac +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (gac *GithubAppCreate) SetNillableID(u *uuid.UUID) *GithubAppCreate { + if u != nil { + gac.SetID(*u) + } + return gac +} + +// Mutation returns the GithubAppMutation object of the builder. +func (gac *GithubAppCreate) Mutation() *GithubAppMutation { + return gac.mutation +} + +// Save creates the GithubApp in the database. +func (gac *GithubAppCreate) Save(ctx context.Context) (*GithubApp, error) { + gac.defaults() + return withHooks(ctx, gac.sqlSave, gac.mutation, gac.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (gac *GithubAppCreate) SaveX(ctx context.Context) *GithubApp { + v, err := gac.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (gac *GithubAppCreate) Exec(ctx context.Context) error { + _, err := gac.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gac *GithubAppCreate) ExecX(ctx context.Context) { + if err := gac.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (gac *GithubAppCreate) defaults() { + if _, ok := gac.mutation.CreatedAt(); !ok { + v := githubapp.DefaultCreatedAt() + gac.mutation.SetCreatedAt(v) + } + if _, ok := gac.mutation.UpdatedAt(); !ok { + v := githubapp.DefaultUpdatedAt() + gac.mutation.SetUpdatedAt(v) + } + if _, ok := gac.mutation.ID(); !ok { + v := githubapp.DefaultID() + gac.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (gac *GithubAppCreate) check() error { + if _, ok := gac.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "GithubApp.created_at"`)} + } + if _, ok := gac.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "GithubApp.updated_at"`)} + } + if _, ok := gac.mutation.GithubAppID(); !ok { + return &ValidationError{Name: "github_app_id", err: errors.New(`ent: missing required field "GithubApp.github_app_id"`)} + } + if _, ok := gac.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "GithubApp.name"`)} + } + if v, ok := gac.mutation.Name(); ok { + if err := githubapp.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "GithubApp.name": %w`, err)} + } + } + if _, ok := gac.mutation.ClientID(); !ok { + return &ValidationError{Name: "client_id", err: errors.New(`ent: missing required field "GithubApp.client_id"`)} + } + if _, ok := gac.mutation.ClientSecret(); !ok { + return &ValidationError{Name: "client_secret", err: errors.New(`ent: missing required field "GithubApp.client_secret"`)} + } + if _, ok := gac.mutation.WebhookSecret(); !ok { + return &ValidationError{Name: "webhook_secret", err: errors.New(`ent: missing required field "GithubApp.webhook_secret"`)} + } + if _, ok := gac.mutation.PrivateKey(); !ok { + return &ValidationError{Name: "private_key", err: errors.New(`ent: missing required field "GithubApp.private_key"`)} + } + return nil +} + +func (gac *GithubAppCreate) sqlSave(ctx context.Context) (*GithubApp, error) { + if err := gac.check(); err != nil { + return nil, err + } + _node, _spec := gac.createSpec() + if err := sqlgraph.CreateNode(ctx, gac.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + gac.mutation.id = &_node.ID + gac.mutation.done = true + return _node, nil +} + +func (gac *GithubAppCreate) createSpec() (*GithubApp, *sqlgraph.CreateSpec) { + var ( + _node = &GithubApp{config: gac.config} + _spec = sqlgraph.NewCreateSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + ) + _spec.OnConflict = gac.conflict + if id, ok := gac.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := gac.mutation.CreatedAt(); ok { + _spec.SetField(githubapp.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := gac.mutation.UpdatedAt(); ok { + _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := gac.mutation.GithubAppID(); ok { + _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) + _node.GithubAppID = value + } + if value, ok := gac.mutation.Name(); ok { + _spec.SetField(githubapp.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := gac.mutation.ClientID(); ok { + _spec.SetField(githubapp.FieldClientID, field.TypeString, value) + _node.ClientID = value + } + if value, ok := gac.mutation.ClientSecret(); ok { + _spec.SetField(githubapp.FieldClientSecret, field.TypeString, value) + _node.ClientSecret = value + } + if value, ok := gac.mutation.WebhookSecret(); ok { + _spec.SetField(githubapp.FieldWebhookSecret, field.TypeString, value) + _node.WebhookSecret = value + } + if value, ok := gac.mutation.PrivateKey(); ok { + _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) + _node.PrivateKey = value + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubApp.Create(). +// SetCreatedAt(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubAppUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (gac *GithubAppCreate) OnConflict(opts ...sql.ConflictOption) *GithubAppUpsertOne { + gac.conflict = opts + return &GithubAppUpsertOne{ + create: gac, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (gac *GithubAppCreate) OnConflictColumns(columns ...string) *GithubAppUpsertOne { + gac.conflict = append(gac.conflict, sql.ConflictColumns(columns...)) + return &GithubAppUpsertOne{ + create: gac, + } +} + +type ( + // GithubAppUpsertOne is the builder for "upsert"-ing + // one GithubApp node. + GithubAppUpsertOne struct { + create *GithubAppCreate + } + + // GithubAppUpsert is the "OnConflict" setter. + GithubAppUpsert struct { + *sql.UpdateSet + } +) + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubAppUpsert) SetUpdatedAt(v time.Time) *GithubAppUpsert { + u.Set(githubapp.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateUpdatedAt() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldUpdatedAt) + return u +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubAppUpsert) SetGithubAppID(v int64) *GithubAppUpsert { + u.Set(githubapp.FieldGithubAppID, v) + return u +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateGithubAppID() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldGithubAppID) + return u +} + +// AddGithubAppID adds v to the "github_app_id" field. +func (u *GithubAppUpsert) AddGithubAppID(v int64) *GithubAppUpsert { + u.Add(githubapp.FieldGithubAppID, v) + return u +} + +// SetName sets the "name" field. +func (u *GithubAppUpsert) SetName(v string) *GithubAppUpsert { + u.Set(githubapp.FieldName, v) + return u +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateName() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldName) + return u +} + +// SetClientID sets the "client_id" field. +func (u *GithubAppUpsert) SetClientID(v string) *GithubAppUpsert { + u.Set(githubapp.FieldClientID, v) + return u +} + +// UpdateClientID sets the "client_id" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateClientID() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldClientID) + return u +} + +// SetClientSecret sets the "client_secret" field. +func (u *GithubAppUpsert) SetClientSecret(v string) *GithubAppUpsert { + u.Set(githubapp.FieldClientSecret, v) + return u +} + +// UpdateClientSecret sets the "client_secret" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateClientSecret() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldClientSecret) + return u +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (u *GithubAppUpsert) SetWebhookSecret(v string) *GithubAppUpsert { + u.Set(githubapp.FieldWebhookSecret, v) + return u +} + +// UpdateWebhookSecret sets the "webhook_secret" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdateWebhookSecret() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldWebhookSecret) + return u +} + +// SetPrivateKey sets the "private_key" field. +func (u *GithubAppUpsert) SetPrivateKey(v string) *GithubAppUpsert { + u.Set(githubapp.FieldPrivateKey, v) + return u +} + +// UpdatePrivateKey sets the "private_key" field to the value that was provided on create. +func (u *GithubAppUpsert) UpdatePrivateKey() *GithubAppUpsert { + u.SetExcluded(githubapp.FieldPrivateKey) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubapp.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubAppUpsertOne) UpdateNewValues() *GithubAppUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(githubapp.FieldID) + } + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(githubapp.FieldCreatedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubAppUpsertOne) Ignore() *GithubAppUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubAppUpsertOne) DoNothing() *GithubAppUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubAppCreate.OnConflict +// documentation for more info. +func (u *GithubAppUpsertOne) Update(set func(*GithubAppUpsert)) *GithubAppUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubAppUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubAppUpsertOne) SetUpdatedAt(v time.Time) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateUpdatedAt() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubAppUpsertOne) SetGithubAppID(v int64) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetGithubAppID(v) + }) +} + +// AddGithubAppID adds v to the "github_app_id" field. +func (u *GithubAppUpsertOne) AddGithubAppID(v int64) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.AddGithubAppID(v) + }) +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateGithubAppID() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateGithubAppID() + }) +} + +// SetName sets the "name" field. +func (u *GithubAppUpsertOne) SetName(v string) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateName() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateName() + }) +} + +// SetClientID sets the "client_id" field. +func (u *GithubAppUpsertOne) SetClientID(v string) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetClientID(v) + }) +} + +// UpdateClientID sets the "client_id" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateClientID() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateClientID() + }) +} + +// SetClientSecret sets the "client_secret" field. +func (u *GithubAppUpsertOne) SetClientSecret(v string) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetClientSecret(v) + }) +} + +// UpdateClientSecret sets the "client_secret" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateClientSecret() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateClientSecret() + }) +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (u *GithubAppUpsertOne) SetWebhookSecret(v string) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetWebhookSecret(v) + }) +} + +// UpdateWebhookSecret sets the "webhook_secret" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdateWebhookSecret() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateWebhookSecret() + }) +} + +// SetPrivateKey sets the "private_key" field. +func (u *GithubAppUpsertOne) SetPrivateKey(v string) *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.SetPrivateKey(v) + }) +} + +// UpdatePrivateKey sets the "private_key" field to the value that was provided on create. +func (u *GithubAppUpsertOne) UpdatePrivateKey() *GithubAppUpsertOne { + return u.Update(func(s *GithubAppUpsert) { + s.UpdatePrivateKey() + }) +} + +// Exec executes the query. +func (u *GithubAppUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubAppCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubAppUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GithubAppUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { + if u.create.driver.Dialect() == dialect.MySQL { + // In case of "ON CONFLICT", there is no way to get back non-numeric ID + // fields from the database since MySQL does not support the RETURNING clause. + return id, errors.New("ent: GithubAppUpsertOne.ID is not supported by MySQL driver. Use GithubAppUpsertOne.Exec instead") + } + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GithubAppUpsertOne) IDX(ctx context.Context) uuid.UUID { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// GithubAppCreateBulk is the builder for creating many GithubApp entities in bulk. +type GithubAppCreateBulk struct { + config + err error + builders []*GithubAppCreate + conflict []sql.ConflictOption +} + +// Save creates the GithubApp entities in the database. +func (gacb *GithubAppCreateBulk) Save(ctx context.Context) ([]*GithubApp, error) { + if gacb.err != nil { + return nil, gacb.err + } + specs := make([]*sqlgraph.CreateSpec, len(gacb.builders)) + nodes := make([]*GithubApp, len(gacb.builders)) + mutators := make([]Mutator, len(gacb.builders)) + for i := range gacb.builders { + func(i int, root context.Context) { + builder := gacb.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*GithubAppMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, gacb.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = gacb.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, gacb.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, gacb.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (gacb *GithubAppCreateBulk) SaveX(ctx context.Context) []*GithubApp { + v, err := gacb.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (gacb *GithubAppCreateBulk) Exec(ctx context.Context) error { + _, err := gacb.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gacb *GithubAppCreateBulk) ExecX(ctx context.Context) { + if err := gacb.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubApp.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubAppUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (gacb *GithubAppCreateBulk) OnConflict(opts ...sql.ConflictOption) *GithubAppUpsertBulk { + gacb.conflict = opts + return &GithubAppUpsertBulk{ + create: gacb, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (gacb *GithubAppCreateBulk) OnConflictColumns(columns ...string) *GithubAppUpsertBulk { + gacb.conflict = append(gacb.conflict, sql.ConflictColumns(columns...)) + return &GithubAppUpsertBulk{ + create: gacb, + } +} + +// GithubAppUpsertBulk is the builder for "upsert"-ing +// a bulk of GithubApp nodes. +type GithubAppUpsertBulk struct { + create *GithubAppCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubapp.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubAppUpsertBulk) UpdateNewValues() *GithubAppUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(githubapp.FieldID) + } + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(githubapp.FieldCreatedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubApp.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubAppUpsertBulk) Ignore() *GithubAppUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubAppUpsertBulk) DoNothing() *GithubAppUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubAppCreateBulk.OnConflict +// documentation for more info. +func (u *GithubAppUpsertBulk) Update(set func(*GithubAppUpsert)) *GithubAppUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubAppUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubAppUpsertBulk) SetUpdatedAt(v time.Time) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateUpdatedAt() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubAppUpsertBulk) SetGithubAppID(v int64) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetGithubAppID(v) + }) +} + +// AddGithubAppID adds v to the "github_app_id" field. +func (u *GithubAppUpsertBulk) AddGithubAppID(v int64) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.AddGithubAppID(v) + }) +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateGithubAppID() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateGithubAppID() + }) +} + +// SetName sets the "name" field. +func (u *GithubAppUpsertBulk) SetName(v string) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetName(v) + }) +} + +// UpdateName sets the "name" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateName() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateName() + }) +} + +// SetClientID sets the "client_id" field. +func (u *GithubAppUpsertBulk) SetClientID(v string) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetClientID(v) + }) +} + +// UpdateClientID sets the "client_id" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateClientID() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateClientID() + }) +} + +// SetClientSecret sets the "client_secret" field. +func (u *GithubAppUpsertBulk) SetClientSecret(v string) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetClientSecret(v) + }) +} + +// UpdateClientSecret sets the "client_secret" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateClientSecret() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateClientSecret() + }) +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (u *GithubAppUpsertBulk) SetWebhookSecret(v string) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetWebhookSecret(v) + }) +} + +// UpdateWebhookSecret sets the "webhook_secret" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdateWebhookSecret() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdateWebhookSecret() + }) +} + +// SetPrivateKey sets the "private_key" field. +func (u *GithubAppUpsertBulk) SetPrivateKey(v string) *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.SetPrivateKey(v) + }) +} + +// UpdatePrivateKey sets the "private_key" field to the value that was provided on create. +func (u *GithubAppUpsertBulk) UpdatePrivateKey() *GithubAppUpsertBulk { + return u.Update(func(s *GithubAppUpsert) { + s.UpdatePrivateKey() + }) +} + +// Exec executes the query. +func (u *GithubAppUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GithubAppCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubAppCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubAppUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/githubapp_delete.go b/ent/githubapp_delete.go new file mode 100644 index 00000000..39a87110 --- /dev/null +++ b/ent/githubapp_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// GithubAppDelete is the builder for deleting a GithubApp entity. +type GithubAppDelete struct { + config + hooks []Hook + mutation *GithubAppMutation +} + +// Where appends a list predicates to the GithubAppDelete builder. +func (gad *GithubAppDelete) Where(ps ...predicate.GithubApp) *GithubAppDelete { + gad.mutation.Where(ps...) + return gad +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (gad *GithubAppDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, gad.sqlExec, gad.mutation, gad.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (gad *GithubAppDelete) ExecX(ctx context.Context) int { + n, err := gad.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (gad *GithubAppDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + if ps := gad.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, gad.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + gad.mutation.done = true + return affected, err +} + +// GithubAppDeleteOne is the builder for deleting a single GithubApp entity. +type GithubAppDeleteOne struct { + gad *GithubAppDelete +} + +// Where appends a list predicates to the GithubAppDelete builder. +func (gado *GithubAppDeleteOne) Where(ps ...predicate.GithubApp) *GithubAppDeleteOne { + gado.gad.mutation.Where(ps...) + return gado +} + +// Exec executes the deletion query. +func (gado *GithubAppDeleteOne) Exec(ctx context.Context) error { + n, err := gado.gad.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{githubapp.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (gado *GithubAppDeleteOne) ExecX(ctx context.Context) { + if err := gado.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/githubapp_query.go b/ent/githubapp_query.go new file mode 100644 index 00000000..94e9d158 --- /dev/null +++ b/ent/githubapp_query.go @@ -0,0 +1,551 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// GithubAppQuery is the builder for querying GithubApp entities. +type GithubAppQuery struct { + config + ctx *QueryContext + order []githubapp.OrderOption + inters []Interceptor + predicates []predicate.GithubApp + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the GithubAppQuery builder. +func (gaq *GithubAppQuery) Where(ps ...predicate.GithubApp) *GithubAppQuery { + gaq.predicates = append(gaq.predicates, ps...) + return gaq +} + +// Limit the number of records to be returned by this query. +func (gaq *GithubAppQuery) Limit(limit int) *GithubAppQuery { + gaq.ctx.Limit = &limit + return gaq +} + +// Offset to start from. +func (gaq *GithubAppQuery) Offset(offset int) *GithubAppQuery { + gaq.ctx.Offset = &offset + return gaq +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (gaq *GithubAppQuery) Unique(unique bool) *GithubAppQuery { + gaq.ctx.Unique = &unique + return gaq +} + +// Order specifies how the records should be ordered. +func (gaq *GithubAppQuery) Order(o ...githubapp.OrderOption) *GithubAppQuery { + gaq.order = append(gaq.order, o...) + return gaq +} + +// First returns the first GithubApp entity from the query. +// Returns a *NotFoundError when no GithubApp was found. +func (gaq *GithubAppQuery) First(ctx context.Context) (*GithubApp, error) { + nodes, err := gaq.Limit(1).All(setContextOp(ctx, gaq.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{githubapp.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (gaq *GithubAppQuery) FirstX(ctx context.Context) *GithubApp { + node, err := gaq.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first GithubApp ID from the query. +// Returns a *NotFoundError when no GithubApp ID was found. +func (gaq *GithubAppQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = gaq.Limit(1).IDs(setContextOp(ctx, gaq.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{githubapp.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (gaq *GithubAppQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := gaq.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single GithubApp entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one GithubApp entity is found. +// Returns a *NotFoundError when no GithubApp entities are found. +func (gaq *GithubAppQuery) Only(ctx context.Context) (*GithubApp, error) { + nodes, err := gaq.Limit(2).All(setContextOp(ctx, gaq.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{githubapp.Label} + default: + return nil, &NotSingularError{githubapp.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (gaq *GithubAppQuery) OnlyX(ctx context.Context) *GithubApp { + node, err := gaq.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only GithubApp ID in the query. +// Returns a *NotSingularError when more than one GithubApp ID is found. +// Returns a *NotFoundError when no entities are found. +func (gaq *GithubAppQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = gaq.Limit(2).IDs(setContextOp(ctx, gaq.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{githubapp.Label} + default: + err = &NotSingularError{githubapp.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (gaq *GithubAppQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := gaq.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of GithubApps. +func (gaq *GithubAppQuery) All(ctx context.Context) ([]*GithubApp, error) { + ctx = setContextOp(ctx, gaq.ctx, ent.OpQueryAll) + if err := gaq.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*GithubApp, *GithubAppQuery]() + return withInterceptors[[]*GithubApp](ctx, gaq, qr, gaq.inters) +} + +// AllX is like All, but panics if an error occurs. +func (gaq *GithubAppQuery) AllX(ctx context.Context) []*GithubApp { + nodes, err := gaq.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of GithubApp IDs. +func (gaq *GithubAppQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if gaq.ctx.Unique == nil && gaq.path != nil { + gaq.Unique(true) + } + ctx = setContextOp(ctx, gaq.ctx, ent.OpQueryIDs) + if err = gaq.Select(githubapp.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (gaq *GithubAppQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := gaq.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (gaq *GithubAppQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, gaq.ctx, ent.OpQueryCount) + if err := gaq.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, gaq, querierCount[*GithubAppQuery](), gaq.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (gaq *GithubAppQuery) CountX(ctx context.Context) int { + count, err := gaq.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (gaq *GithubAppQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, gaq.ctx, ent.OpQueryExist) + switch _, err := gaq.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (gaq *GithubAppQuery) ExistX(ctx context.Context) bool { + exist, err := gaq.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the GithubAppQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (gaq *GithubAppQuery) Clone() *GithubAppQuery { + if gaq == nil { + return nil + } + return &GithubAppQuery{ + config: gaq.config, + ctx: gaq.ctx.Clone(), + order: append([]githubapp.OrderOption{}, gaq.order...), + inters: append([]Interceptor{}, gaq.inters...), + predicates: append([]predicate.GithubApp{}, gaq.predicates...), + // clone intermediate query. + sql: gaq.sql.Clone(), + path: gaq.path, + modifiers: append([]func(*sql.Selector){}, gaq.modifiers...), + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.GithubApp.Query(). +// GroupBy(githubapp.FieldCreatedAt). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (gaq *GithubAppQuery) GroupBy(field string, fields ...string) *GithubAppGroupBy { + gaq.ctx.Fields = append([]string{field}, fields...) + grbuild := &GithubAppGroupBy{build: gaq} + grbuild.flds = &gaq.ctx.Fields + grbuild.label = githubapp.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// } +// +// client.GithubApp.Query(). +// Select(githubapp.FieldCreatedAt). +// Scan(ctx, &v) +func (gaq *GithubAppQuery) Select(fields ...string) *GithubAppSelect { + gaq.ctx.Fields = append(gaq.ctx.Fields, fields...) + sbuild := &GithubAppSelect{GithubAppQuery: gaq} + sbuild.label = githubapp.Label + sbuild.flds, sbuild.scan = &gaq.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a GithubAppSelect configured with the given aggregations. +func (gaq *GithubAppQuery) Aggregate(fns ...AggregateFunc) *GithubAppSelect { + return gaq.Select().Aggregate(fns...) +} + +func (gaq *GithubAppQuery) prepareQuery(ctx context.Context) error { + for _, inter := range gaq.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, gaq); err != nil { + return err + } + } + } + for _, f := range gaq.ctx.Fields { + if !githubapp.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if gaq.path != nil { + prev, err := gaq.path(ctx) + if err != nil { + return err + } + gaq.sql = prev + } + return nil +} + +func (gaq *GithubAppQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*GithubApp, error) { + var ( + nodes = []*GithubApp{} + _spec = gaq.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*GithubApp).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &GithubApp{config: gaq.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + if len(gaq.modifiers) > 0 { + _spec.Modifiers = gaq.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, gaq.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (gaq *GithubAppQuery) sqlCount(ctx context.Context) (int, error) { + _spec := gaq.querySpec() + if len(gaq.modifiers) > 0 { + _spec.Modifiers = gaq.modifiers + } + _spec.Node.Columns = gaq.ctx.Fields + if len(gaq.ctx.Fields) > 0 { + _spec.Unique = gaq.ctx.Unique != nil && *gaq.ctx.Unique + } + return sqlgraph.CountNodes(ctx, gaq.driver, _spec) +} + +func (gaq *GithubAppQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec.From = gaq.sql + if unique := gaq.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if gaq.path != nil { + _spec.Unique = true + } + if fields := gaq.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubapp.FieldID) + for i := range fields { + if fields[i] != githubapp.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := gaq.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := gaq.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := gaq.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := gaq.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (gaq *GithubAppQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(gaq.driver.Dialect()) + t1 := builder.Table(githubapp.Table) + columns := gaq.ctx.Fields + if len(columns) == 0 { + columns = githubapp.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if gaq.sql != nil { + selector = gaq.sql + selector.Select(selector.Columns(columns...)...) + } + if gaq.ctx.Unique != nil && *gaq.ctx.Unique { + selector.Distinct() + } + for _, m := range gaq.modifiers { + m(selector) + } + for _, p := range gaq.predicates { + p(selector) + } + for _, p := range gaq.order { + p(selector) + } + if offset := gaq.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := gaq.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (gaq *GithubAppQuery) Modify(modifiers ...func(s *sql.Selector)) *GithubAppSelect { + gaq.modifiers = append(gaq.modifiers, modifiers...) + return gaq.Select() +} + +// GithubAppGroupBy is the group-by builder for GithubApp entities. +type GithubAppGroupBy struct { + selector + build *GithubAppQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (gagb *GithubAppGroupBy) Aggregate(fns ...AggregateFunc) *GithubAppGroupBy { + gagb.fns = append(gagb.fns, fns...) + return gagb +} + +// Scan applies the selector query and scans the result into the given value. +func (gagb *GithubAppGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, gagb.build.ctx, ent.OpQueryGroupBy) + if err := gagb.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubAppQuery, *GithubAppGroupBy](ctx, gagb.build, gagb, gagb.build.inters, v) +} + +func (gagb *GithubAppGroupBy) sqlScan(ctx context.Context, root *GithubAppQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(gagb.fns)) + for _, fn := range gagb.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*gagb.flds)+len(gagb.fns)) + for _, f := range *gagb.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*gagb.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := gagb.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// GithubAppSelect is the builder for selecting fields of GithubApp entities. +type GithubAppSelect struct { + *GithubAppQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (gas *GithubAppSelect) Aggregate(fns ...AggregateFunc) *GithubAppSelect { + gas.fns = append(gas.fns, fns...) + return gas +} + +// Scan applies the selector query and scans the result into the given value. +func (gas *GithubAppSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, gas.ctx, ent.OpQuerySelect) + if err := gas.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubAppQuery, *GithubAppSelect](ctx, gas.GithubAppQuery, gas, gas.inters, v) +} + +func (gas *GithubAppSelect) sqlScan(ctx context.Context, root *GithubAppQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(gas.fns)) + for _, fn := range gas.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*gas.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := gas.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (gas *GithubAppSelect) Modify(modifiers ...func(s *sql.Selector)) *GithubAppSelect { + gas.modifiers = append(gas.modifiers, modifiers...) + return gas +} diff --git a/ent/githubapp_update.go b/ent/githubapp_update.go new file mode 100644 index 00000000..7e51bf3d --- /dev/null +++ b/ent/githubapp_update.go @@ -0,0 +1,478 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// GithubAppUpdate is the builder for updating GithubApp entities. +type GithubAppUpdate struct { + config + hooks []Hook + mutation *GithubAppMutation + modifiers []func(*sql.UpdateBuilder) +} + +// Where appends a list predicates to the GithubAppUpdate builder. +func (gau *GithubAppUpdate) Where(ps ...predicate.GithubApp) *GithubAppUpdate { + gau.mutation.Where(ps...) + return gau +} + +// SetUpdatedAt sets the "updated_at" field. +func (gau *GithubAppUpdate) SetUpdatedAt(t time.Time) *GithubAppUpdate { + gau.mutation.SetUpdatedAt(t) + return gau +} + +// SetGithubAppID sets the "github_app_id" field. +func (gau *GithubAppUpdate) SetGithubAppID(i int64) *GithubAppUpdate { + gau.mutation.ResetGithubAppID() + gau.mutation.SetGithubAppID(i) + return gau +} + +// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillableGithubAppID(i *int64) *GithubAppUpdate { + if i != nil { + gau.SetGithubAppID(*i) + } + return gau +} + +// AddGithubAppID adds i to the "github_app_id" field. +func (gau *GithubAppUpdate) AddGithubAppID(i int64) *GithubAppUpdate { + gau.mutation.AddGithubAppID(i) + return gau +} + +// SetName sets the "name" field. +func (gau *GithubAppUpdate) SetName(s string) *GithubAppUpdate { + gau.mutation.SetName(s) + return gau +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillableName(s *string) *GithubAppUpdate { + if s != nil { + gau.SetName(*s) + } + return gau +} + +// SetClientID sets the "client_id" field. +func (gau *GithubAppUpdate) SetClientID(s string) *GithubAppUpdate { + gau.mutation.SetClientID(s) + return gau +} + +// SetNillableClientID sets the "client_id" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillableClientID(s *string) *GithubAppUpdate { + if s != nil { + gau.SetClientID(*s) + } + return gau +} + +// SetClientSecret sets the "client_secret" field. +func (gau *GithubAppUpdate) SetClientSecret(s string) *GithubAppUpdate { + gau.mutation.SetClientSecret(s) + return gau +} + +// SetNillableClientSecret sets the "client_secret" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillableClientSecret(s *string) *GithubAppUpdate { + if s != nil { + gau.SetClientSecret(*s) + } + return gau +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (gau *GithubAppUpdate) SetWebhookSecret(s string) *GithubAppUpdate { + gau.mutation.SetWebhookSecret(s) + return gau +} + +// SetNillableWebhookSecret sets the "webhook_secret" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillableWebhookSecret(s *string) *GithubAppUpdate { + if s != nil { + gau.SetWebhookSecret(*s) + } + return gau +} + +// SetPrivateKey sets the "private_key" field. +func (gau *GithubAppUpdate) SetPrivateKey(s string) *GithubAppUpdate { + gau.mutation.SetPrivateKey(s) + return gau +} + +// SetNillablePrivateKey sets the "private_key" field if the given value is not nil. +func (gau *GithubAppUpdate) SetNillablePrivateKey(s *string) *GithubAppUpdate { + if s != nil { + gau.SetPrivateKey(*s) + } + return gau +} + +// Mutation returns the GithubAppMutation object of the builder. +func (gau *GithubAppUpdate) Mutation() *GithubAppMutation { + return gau.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (gau *GithubAppUpdate) Save(ctx context.Context) (int, error) { + gau.defaults() + return withHooks(ctx, gau.sqlSave, gau.mutation, gau.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (gau *GithubAppUpdate) SaveX(ctx context.Context) int { + affected, err := gau.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (gau *GithubAppUpdate) Exec(ctx context.Context) error { + _, err := gau.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gau *GithubAppUpdate) ExecX(ctx context.Context) { + if err := gau.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (gau *GithubAppUpdate) defaults() { + if _, ok := gau.mutation.UpdatedAt(); !ok { + v := githubapp.UpdateDefaultUpdatedAt() + gau.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (gau *GithubAppUpdate) check() error { + if v, ok := gau.mutation.Name(); ok { + if err := githubapp.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "GithubApp.name": %w`, err)} + } + } + return nil +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (gau *GithubAppUpdate) Modify(modifiers ...func(u *sql.UpdateBuilder)) *GithubAppUpdate { + gau.modifiers = append(gau.modifiers, modifiers...) + return gau +} + +func (gau *GithubAppUpdate) sqlSave(ctx context.Context) (n int, err error) { + if err := gau.check(); err != nil { + return n, err + } + _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + if ps := gau.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := gau.mutation.UpdatedAt(); ok { + _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := gau.mutation.GithubAppID(); ok { + _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) + } + if value, ok := gau.mutation.AddedGithubAppID(); ok { + _spec.AddField(githubapp.FieldGithubAppID, field.TypeInt64, value) + } + if value, ok := gau.mutation.Name(); ok { + _spec.SetField(githubapp.FieldName, field.TypeString, value) + } + if value, ok := gau.mutation.ClientID(); ok { + _spec.SetField(githubapp.FieldClientID, field.TypeString, value) + } + if value, ok := gau.mutation.ClientSecret(); ok { + _spec.SetField(githubapp.FieldClientSecret, field.TypeString, value) + } + if value, ok := gau.mutation.WebhookSecret(); ok { + _spec.SetField(githubapp.FieldWebhookSecret, field.TypeString, value) + } + if value, ok := gau.mutation.PrivateKey(); ok { + _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) + } + _spec.AddModifiers(gau.modifiers...) + if n, err = sqlgraph.UpdateNodes(ctx, gau.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubapp.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + gau.mutation.done = true + return n, nil +} + +// GithubAppUpdateOne is the builder for updating a single GithubApp entity. +type GithubAppUpdateOne struct { + config + fields []string + hooks []Hook + mutation *GithubAppMutation + modifiers []func(*sql.UpdateBuilder) +} + +// SetUpdatedAt sets the "updated_at" field. +func (gauo *GithubAppUpdateOne) SetUpdatedAt(t time.Time) *GithubAppUpdateOne { + gauo.mutation.SetUpdatedAt(t) + return gauo +} + +// SetGithubAppID sets the "github_app_id" field. +func (gauo *GithubAppUpdateOne) SetGithubAppID(i int64) *GithubAppUpdateOne { + gauo.mutation.ResetGithubAppID() + gauo.mutation.SetGithubAppID(i) + return gauo +} + +// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillableGithubAppID(i *int64) *GithubAppUpdateOne { + if i != nil { + gauo.SetGithubAppID(*i) + } + return gauo +} + +// AddGithubAppID adds i to the "github_app_id" field. +func (gauo *GithubAppUpdateOne) AddGithubAppID(i int64) *GithubAppUpdateOne { + gauo.mutation.AddGithubAppID(i) + return gauo +} + +// SetName sets the "name" field. +func (gauo *GithubAppUpdateOne) SetName(s string) *GithubAppUpdateOne { + gauo.mutation.SetName(s) + return gauo +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillableName(s *string) *GithubAppUpdateOne { + if s != nil { + gauo.SetName(*s) + } + return gauo +} + +// SetClientID sets the "client_id" field. +func (gauo *GithubAppUpdateOne) SetClientID(s string) *GithubAppUpdateOne { + gauo.mutation.SetClientID(s) + return gauo +} + +// SetNillableClientID sets the "client_id" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillableClientID(s *string) *GithubAppUpdateOne { + if s != nil { + gauo.SetClientID(*s) + } + return gauo +} + +// SetClientSecret sets the "client_secret" field. +func (gauo *GithubAppUpdateOne) SetClientSecret(s string) *GithubAppUpdateOne { + gauo.mutation.SetClientSecret(s) + return gauo +} + +// SetNillableClientSecret sets the "client_secret" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillableClientSecret(s *string) *GithubAppUpdateOne { + if s != nil { + gauo.SetClientSecret(*s) + } + return gauo +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (gauo *GithubAppUpdateOne) SetWebhookSecret(s string) *GithubAppUpdateOne { + gauo.mutation.SetWebhookSecret(s) + return gauo +} + +// SetNillableWebhookSecret sets the "webhook_secret" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillableWebhookSecret(s *string) *GithubAppUpdateOne { + if s != nil { + gauo.SetWebhookSecret(*s) + } + return gauo +} + +// SetPrivateKey sets the "private_key" field. +func (gauo *GithubAppUpdateOne) SetPrivateKey(s string) *GithubAppUpdateOne { + gauo.mutation.SetPrivateKey(s) + return gauo +} + +// SetNillablePrivateKey sets the "private_key" field if the given value is not nil. +func (gauo *GithubAppUpdateOne) SetNillablePrivateKey(s *string) *GithubAppUpdateOne { + if s != nil { + gauo.SetPrivateKey(*s) + } + return gauo +} + +// Mutation returns the GithubAppMutation object of the builder. +func (gauo *GithubAppUpdateOne) Mutation() *GithubAppMutation { + return gauo.mutation +} + +// Where appends a list predicates to the GithubAppUpdate builder. +func (gauo *GithubAppUpdateOne) Where(ps ...predicate.GithubApp) *GithubAppUpdateOne { + gauo.mutation.Where(ps...) + return gauo +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (gauo *GithubAppUpdateOne) Select(field string, fields ...string) *GithubAppUpdateOne { + gauo.fields = append([]string{field}, fields...) + return gauo +} + +// Save executes the query and returns the updated GithubApp entity. +func (gauo *GithubAppUpdateOne) Save(ctx context.Context) (*GithubApp, error) { + gauo.defaults() + return withHooks(ctx, gauo.sqlSave, gauo.mutation, gauo.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (gauo *GithubAppUpdateOne) SaveX(ctx context.Context) *GithubApp { + node, err := gauo.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (gauo *GithubAppUpdateOne) Exec(ctx context.Context) error { + _, err := gauo.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gauo *GithubAppUpdateOne) ExecX(ctx context.Context) { + if err := gauo.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (gauo *GithubAppUpdateOne) defaults() { + if _, ok := gauo.mutation.UpdatedAt(); !ok { + v := githubapp.UpdateDefaultUpdatedAt() + gauo.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (gauo *GithubAppUpdateOne) check() error { + if v, ok := gauo.mutation.Name(); ok { + if err := githubapp.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "GithubApp.name": %w`, err)} + } + } + return nil +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (gauo *GithubAppUpdateOne) Modify(modifiers ...func(u *sql.UpdateBuilder)) *GithubAppUpdateOne { + gauo.modifiers = append(gauo.modifiers, modifiers...) + return gauo +} + +func (gauo *GithubAppUpdateOne) sqlSave(ctx context.Context) (_node *GithubApp, err error) { + if err := gauo.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + id, ok := gauo.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "GithubApp.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := gauo.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubapp.FieldID) + for _, f := range fields { + if !githubapp.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != githubapp.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := gauo.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := gauo.mutation.UpdatedAt(); ok { + _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := gauo.mutation.GithubAppID(); ok { + _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) + } + if value, ok := gauo.mutation.AddedGithubAppID(); ok { + _spec.AddField(githubapp.FieldGithubAppID, field.TypeInt64, value) + } + if value, ok := gauo.mutation.Name(); ok { + _spec.SetField(githubapp.FieldName, field.TypeString, value) + } + if value, ok := gauo.mutation.ClientID(); ok { + _spec.SetField(githubapp.FieldClientID, field.TypeString, value) + } + if value, ok := gauo.mutation.ClientSecret(); ok { + _spec.SetField(githubapp.FieldClientSecret, field.TypeString, value) + } + if value, ok := gauo.mutation.WebhookSecret(); ok { + _spec.SetField(githubapp.FieldWebhookSecret, field.TypeString, value) + } + if value, ok := gauo.mutation.PrivateKey(); ok { + _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) + } + _spec.AddModifiers(gauo.modifiers...) + _node = &GithubApp{config: gauo.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, gauo.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubapp.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + gauo.mutation.done = true + return _node, nil +} diff --git a/ent/hook/hook.go b/ent/hook/hook.go index 1d3d45e4..df771012 100644 --- a/ent/hook/hook.go +++ b/ent/hook/hook.go @@ -9,6 +9,18 @@ import ( "github.com/unbindapp/unbind-api/ent" ) +// The GithubAppFunc type is an adapter to allow the use of ordinary +// function as GithubApp mutator. +type GithubAppFunc func(context.Context, *ent.GithubAppMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f GithubAppFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.GithubAppMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GithubAppMutation", m) +} + // The UserFunc type is an adapter to allow the use of ordinary // function as User mutator. type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error) diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 4d98ace5..28b8a6bd 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -3,11 +3,37 @@ package migrate import ( + "entgo.io/ent/dialect/entsql" "entgo.io/ent/dialect/sql/schema" "entgo.io/ent/schema/field" ) var ( + // GithubAppsColumns holds the columns for the "github_apps" table. + GithubAppsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID, Unique: true}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "github_app_id", Type: field.TypeInt64, Unique: true}, + {Name: "name", Type: field.TypeString}, + {Name: "client_id", Type: field.TypeString}, + {Name: "client_secret", Type: field.TypeString}, + {Name: "webhook_secret", Type: field.TypeString}, + {Name: "private_key", Type: field.TypeString, Size: 2147483647}, + } + // GithubAppsTable holds the schema information for the "github_apps" table. + GithubAppsTable = &schema.Table{ + Name: "github_apps", + Columns: GithubAppsColumns, + PrimaryKey: []*schema.Column{GithubAppsColumns[0]}, + Indexes: []*schema.Index{ + { + Name: "githubapp_github_app_id", + Unique: true, + Columns: []*schema.Column{GithubAppsColumns[3]}, + }, + }, + } // UsersColumns holds the columns for the "users" table. UsersColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, @@ -25,9 +51,16 @@ var ( } // Tables holds all the tables in the schema. Tables = []*schema.Table{ + GithubAppsTable, UsersTable, } ) func init() { + GithubAppsTable.Annotation = &entsql.Annotation{ + Table: "github_apps", + } + UsersTable.Annotation = &entsql.Annotation{ + Table: "users", + } } diff --git a/ent/mutation.go b/ent/mutation.go index a5390820..1b512508 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/githubapp" "github.com/unbindapp/unbind-api/ent/predicate" "github.com/unbindapp/unbind-api/ent/user" ) @@ -25,9 +26,756 @@ const ( OpUpdateOne = ent.OpUpdateOne // Node types. - TypeUser = "User" + TypeGithubApp = "GithubApp" + TypeUser = "User" ) +// GithubAppMutation represents an operation that mutates the GithubApp nodes in the graph. +type GithubAppMutation struct { + config + op Op + typ string + id *uuid.UUID + created_at *time.Time + updated_at *time.Time + github_app_id *int64 + addgithub_app_id *int64 + name *string + client_id *string + client_secret *string + webhook_secret *string + private_key *string + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*GithubApp, error) + predicates []predicate.GithubApp +} + +var _ ent.Mutation = (*GithubAppMutation)(nil) + +// githubappOption allows management of the mutation configuration using functional options. +type githubappOption func(*GithubAppMutation) + +// newGithubAppMutation creates new mutation for the GithubApp entity. +func newGithubAppMutation(c config, op Op, opts ...githubappOption) *GithubAppMutation { + m := &GithubAppMutation{ + config: c, + op: op, + typ: TypeGithubApp, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGithubAppID sets the ID field of the mutation. +func withGithubAppID(id uuid.UUID) githubappOption { + return func(m *GithubAppMutation) { + var ( + err error + once sync.Once + value *GithubApp + ) + m.oldValue = func(ctx context.Context) (*GithubApp, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().GithubApp.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGithubApp sets the old GithubApp of the mutation. +func withGithubApp(node *GithubApp) githubappOption { + return func(m *GithubAppMutation) { + m.oldValue = func(context.Context) (*GithubApp, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GithubAppMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GithubAppMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of GithubApp entities. +func (m *GithubAppMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GithubAppMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GithubAppMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().GithubApp.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetCreatedAt sets the "created_at" field. +func (m *GithubAppMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *GithubAppMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *GithubAppMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *GithubAppMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *GithubAppMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *GithubAppMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetGithubAppID sets the "github_app_id" field. +func (m *GithubAppMutation) SetGithubAppID(i int64) { + m.github_app_id = &i + m.addgithub_app_id = nil +} + +// GithubAppID returns the value of the "github_app_id" field in the mutation. +func (m *GithubAppMutation) GithubAppID() (r int64, exists bool) { + v := m.github_app_id + if v == nil { + return + } + return *v, true +} + +// OldGithubAppID returns the old "github_app_id" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldGithubAppID(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGithubAppID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGithubAppID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGithubAppID: %w", err) + } + return oldValue.GithubAppID, nil +} + +// AddGithubAppID adds i to the "github_app_id" field. +func (m *GithubAppMutation) AddGithubAppID(i int64) { + if m.addgithub_app_id != nil { + *m.addgithub_app_id += i + } else { + m.addgithub_app_id = &i + } +} + +// AddedGithubAppID returns the value that was added to the "github_app_id" field in this mutation. +func (m *GithubAppMutation) AddedGithubAppID() (r int64, exists bool) { + v := m.addgithub_app_id + if v == nil { + return + } + return *v, true +} + +// ResetGithubAppID resets all changes to the "github_app_id" field. +func (m *GithubAppMutation) ResetGithubAppID() { + m.github_app_id = nil + m.addgithub_app_id = nil +} + +// SetName sets the "name" field. +func (m *GithubAppMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *GithubAppMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *GithubAppMutation) ResetName() { + m.name = nil +} + +// SetClientID sets the "client_id" field. +func (m *GithubAppMutation) SetClientID(s string) { + m.client_id = &s +} + +// ClientID returns the value of the "client_id" field in the mutation. +func (m *GithubAppMutation) ClientID() (r string, exists bool) { + v := m.client_id + if v == nil { + return + } + return *v, true +} + +// OldClientID returns the old "client_id" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldClientID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldClientID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldClientID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldClientID: %w", err) + } + return oldValue.ClientID, nil +} + +// ResetClientID resets all changes to the "client_id" field. +func (m *GithubAppMutation) ResetClientID() { + m.client_id = nil +} + +// SetClientSecret sets the "client_secret" field. +func (m *GithubAppMutation) SetClientSecret(s string) { + m.client_secret = &s +} + +// ClientSecret returns the value of the "client_secret" field in the mutation. +func (m *GithubAppMutation) ClientSecret() (r string, exists bool) { + v := m.client_secret + if v == nil { + return + } + return *v, true +} + +// OldClientSecret returns the old "client_secret" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldClientSecret(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldClientSecret is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldClientSecret requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldClientSecret: %w", err) + } + return oldValue.ClientSecret, nil +} + +// ResetClientSecret resets all changes to the "client_secret" field. +func (m *GithubAppMutation) ResetClientSecret() { + m.client_secret = nil +} + +// SetWebhookSecret sets the "webhook_secret" field. +func (m *GithubAppMutation) SetWebhookSecret(s string) { + m.webhook_secret = &s +} + +// WebhookSecret returns the value of the "webhook_secret" field in the mutation. +func (m *GithubAppMutation) WebhookSecret() (r string, exists bool) { + v := m.webhook_secret + if v == nil { + return + } + return *v, true +} + +// OldWebhookSecret returns the old "webhook_secret" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldWebhookSecret(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldWebhookSecret is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldWebhookSecret requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldWebhookSecret: %w", err) + } + return oldValue.WebhookSecret, nil +} + +// ResetWebhookSecret resets all changes to the "webhook_secret" field. +func (m *GithubAppMutation) ResetWebhookSecret() { + m.webhook_secret = nil +} + +// SetPrivateKey sets the "private_key" field. +func (m *GithubAppMutation) SetPrivateKey(s string) { + m.private_key = &s +} + +// PrivateKey returns the value of the "private_key" field in the mutation. +func (m *GithubAppMutation) PrivateKey() (r string, exists bool) { + v := m.private_key + if v == nil { + return + } + return *v, true +} + +// OldPrivateKey returns the old "private_key" field's value of the GithubApp entity. +// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubAppMutation) OldPrivateKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPrivateKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPrivateKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPrivateKey: %w", err) + } + return oldValue.PrivateKey, nil +} + +// ResetPrivateKey resets all changes to the "private_key" field. +func (m *GithubAppMutation) ResetPrivateKey() { + m.private_key = nil +} + +// Where appends a list predicates to the GithubAppMutation builder. +func (m *GithubAppMutation) Where(ps ...predicate.GithubApp) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GithubAppMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GithubAppMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.GithubApp, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GithubAppMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GithubAppMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (GithubApp). +func (m *GithubAppMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GithubAppMutation) Fields() []string { + fields := make([]string, 0, 8) + if m.created_at != nil { + fields = append(fields, githubapp.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, githubapp.FieldUpdatedAt) + } + if m.github_app_id != nil { + fields = append(fields, githubapp.FieldGithubAppID) + } + if m.name != nil { + fields = append(fields, githubapp.FieldName) + } + if m.client_id != nil { + fields = append(fields, githubapp.FieldClientID) + } + if m.client_secret != nil { + fields = append(fields, githubapp.FieldClientSecret) + } + if m.webhook_secret != nil { + fields = append(fields, githubapp.FieldWebhookSecret) + } + if m.private_key != nil { + fields = append(fields, githubapp.FieldPrivateKey) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GithubAppMutation) Field(name string) (ent.Value, bool) { + switch name { + case githubapp.FieldCreatedAt: + return m.CreatedAt() + case githubapp.FieldUpdatedAt: + return m.UpdatedAt() + case githubapp.FieldGithubAppID: + return m.GithubAppID() + case githubapp.FieldName: + return m.Name() + case githubapp.FieldClientID: + return m.ClientID() + case githubapp.FieldClientSecret: + return m.ClientSecret() + case githubapp.FieldWebhookSecret: + return m.WebhookSecret() + case githubapp.FieldPrivateKey: + return m.PrivateKey() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GithubAppMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case githubapp.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case githubapp.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case githubapp.FieldGithubAppID: + return m.OldGithubAppID(ctx) + case githubapp.FieldName: + return m.OldName(ctx) + case githubapp.FieldClientID: + return m.OldClientID(ctx) + case githubapp.FieldClientSecret: + return m.OldClientSecret(ctx) + case githubapp.FieldWebhookSecret: + return m.OldWebhookSecret(ctx) + case githubapp.FieldPrivateKey: + return m.OldPrivateKey(ctx) + } + return nil, fmt.Errorf("unknown GithubApp field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubAppMutation) SetField(name string, value ent.Value) error { + switch name { + case githubapp.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case githubapp.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case githubapp.FieldGithubAppID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGithubAppID(v) + return nil + case githubapp.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case githubapp.FieldClientID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetClientID(v) + return nil + case githubapp.FieldClientSecret: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetClientSecret(v) + return nil + case githubapp.FieldWebhookSecret: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetWebhookSecret(v) + return nil + case githubapp.FieldPrivateKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPrivateKey(v) + return nil + } + return fmt.Errorf("unknown GithubApp field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GithubAppMutation) AddedFields() []string { + var fields []string + if m.addgithub_app_id != nil { + fields = append(fields, githubapp.FieldGithubAppID) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GithubAppMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case githubapp.FieldGithubAppID: + return m.AddedGithubAppID() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubAppMutation) AddField(name string, value ent.Value) error { + switch name { + case githubapp.FieldGithubAppID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddGithubAppID(v) + return nil + } + return fmt.Errorf("unknown GithubApp numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GithubAppMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GithubAppMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GithubAppMutation) ClearField(name string) error { + return fmt.Errorf("unknown GithubApp nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GithubAppMutation) ResetField(name string) error { + switch name { + case githubapp.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case githubapp.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case githubapp.FieldGithubAppID: + m.ResetGithubAppID() + return nil + case githubapp.FieldName: + m.ResetName() + return nil + case githubapp.FieldClientID: + m.ResetClientID() + return nil + case githubapp.FieldClientSecret: + m.ResetClientSecret() + return nil + case githubapp.FieldWebhookSecret: + m.ResetWebhookSecret() + return nil + case githubapp.FieldPrivateKey: + m.ResetPrivateKey() + return nil + } + return fmt.Errorf("unknown GithubApp field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GithubAppMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GithubAppMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GithubAppMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GithubAppMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GithubAppMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GithubAppMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GithubAppMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown GithubApp unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GithubAppMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown GithubApp edge %s", name) +} + // UserMutation represents an operation that mutates the User nodes in the graph. type UserMutation struct { config diff --git a/ent/predicate/predicate.go b/ent/predicate/predicate.go index af21dfe3..022907f7 100644 --- a/ent/predicate/predicate.go +++ b/ent/predicate/predicate.go @@ -6,5 +6,8 @@ import ( "entgo.io/ent/dialect/sql" ) +// GithubApp is the predicate function for githubapp builders. +type GithubApp func(*sql.Selector) + // User is the predicate function for user builders. type User func(*sql.Selector) diff --git a/ent/runtime.go b/ent/runtime.go index 7bc427e4..133421c8 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/unbindapp/unbind-api/ent/githubapp" "github.com/unbindapp/unbind-api/ent/schema" "github.com/unbindapp/unbind-api/ent/user" ) @@ -14,6 +15,31 @@ import ( // (default values, validators, hooks and policies) and stitches it // to their package variables. func init() { + githubappMixin := schema.GithubApp{}.Mixin() + githubappMixinFields0 := githubappMixin[0].Fields() + _ = githubappMixinFields0 + githubappMixinFields1 := githubappMixin[1].Fields() + _ = githubappMixinFields1 + githubappFields := schema.GithubApp{}.Fields() + _ = githubappFields + // githubappDescCreatedAt is the schema descriptor for created_at field. + githubappDescCreatedAt := githubappMixinFields1[0].Descriptor() + // githubapp.DefaultCreatedAt holds the default value on creation for the created_at field. + githubapp.DefaultCreatedAt = githubappDescCreatedAt.Default.(func() time.Time) + // githubappDescUpdatedAt is the schema descriptor for updated_at field. + githubappDescUpdatedAt := githubappMixinFields1[1].Descriptor() + // githubapp.DefaultUpdatedAt holds the default value on creation for the updated_at field. + githubapp.DefaultUpdatedAt = githubappDescUpdatedAt.Default.(func() time.Time) + // githubapp.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + githubapp.UpdateDefaultUpdatedAt = githubappDescUpdatedAt.UpdateDefault.(func() time.Time) + // githubappDescName is the schema descriptor for name field. + githubappDescName := githubappFields[1].Descriptor() + // githubapp.NameValidator is a validator for the "name" field. It is called by the builders before save. + githubapp.NameValidator = githubappDescName.Validators[0].(func(string) error) + // githubappDescID is the schema descriptor for id field. + githubappDescID := githubappMixinFields0[0].Descriptor() + // githubapp.DefaultID holds the default value on creation for the id field. + githubapp.DefaultID = githubappDescID.Default.(func() uuid.UUID) userMixin := schema.User{}.Mixin() userMixinFields0 := userMixin[0].Fields() _ = userMixinFields0 diff --git a/ent/schema/github_app.go b/ent/schema/github_app.go new file mode 100644 index 00000000..2186f883 --- /dev/null +++ b/ent/schema/github_app.go @@ -0,0 +1,68 @@ +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/unbindapp/unbind-api/ent/schema/mixin" +) + +// GithubApp holds the schema definition for the GithubApp entity. +type GithubApp struct { + ent.Schema +} + +// Mixin of the GithubApp. +func (GithubApp) Mixin() []ent.Mixin { + return []ent.Mixin{ + mixin.PKMixin{}, + mixin.TimeMixin{}, + } +} + +// Fields of the GithubApp. +func (GithubApp) Fields() []ent.Field { + return []ent.Field{ + field.Int64("github_app_id"). + Unique(). + Comment("The GitHub App ID"), + field.String("name"). + NotEmpty(). + Comment("Name of the GitHub App"), + field.String("client_id"). + Comment("OAuth client ID of the GitHub App"), + field.String("client_secret"). + Sensitive(). + Comment("OAuth client secret of the GitHub App"), + field.String("webhook_secret"). + Sensitive(). + Comment("Webhook secret for GitHub events"), + field.Text("private_key"). + Sensitive(). + Comment("Private key (PEM) for GitHub App authentication"), + } +} + +// Edges of the GithubApp. +func (GithubApp) Edges() []ent.Edge { + return nil +} + +// Indexes of the GithubApp. +func (GithubApp) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("github_app_id"). + Unique(), + } +} + +// Annotations of the GithubApp +func (GithubApp) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{ + Table: "github_apps", + }, + } +} diff --git a/ent/schema/user.go b/ent/schema/user.go index 35ed8ea1..1c2e7df3 100644 --- a/ent/schema/user.go +++ b/ent/schema/user.go @@ -2,6 +2,8 @@ package schema import ( "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" "entgo.io/ent/schema/field" "github.com/unbindapp/unbind-api/ent/schema/mixin" ) @@ -34,3 +36,12 @@ func (User) Fields() []ent.Field { func (User) Edges() []ent.Edge { return nil } + +// Annotations of the User +func (User) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{ + Table: "users", + }, + } +} diff --git a/ent/tx.go b/ent/tx.go index 3e6f77f4..d2eed430 100644 --- a/ent/tx.go +++ b/ent/tx.go @@ -14,6 +14,8 @@ import ( // Tx is a transactional client that is created by calling Client.Tx(). type Tx struct { config + // GithubApp is the client for interacting with the GithubApp builders. + GithubApp *GithubAppClient // User is the client for interacting with the User builders. User *UserClient @@ -147,6 +149,7 @@ func (tx *Tx) Client() *Client { } func (tx *Tx) init() { + tx.GithubApp = NewGithubAppClient(tx.config) tx.User = NewUserClient(tx.config) } @@ -157,7 +160,7 @@ func (tx *Tx) init() { // of them in order to commit or rollback the transaction. // // If a closed transaction is embedded in one of the generated entities, and the entity -// applies a query, for example: User.QueryXXX(), the query will be executed +// applies a query, for example: GithubApp.QueryXXX(), the query will be executed // through the driver which created this transaction. // // Note that txDriver is not goroutine safe. diff --git a/go.mod b/go.mod index 3c7265ce..d3a0d16a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 github.com/coreos/go-oidc/v3 v3.12.0 - github.com/danielgtaylor/huma/v2 v2.28.0 + github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb github.com/go-chi/chi/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.2 @@ -37,6 +37,8 @@ require ( github.com/go-openapi/inflect v0.19.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v69 v69.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/hcl/v2 v2.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/go.sum b/go.sum index 044cf69f..ec687b61 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,10 @@ github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/danielgtaylor/huma/v2 v2.28.0 h1:W+hIT52MigO73edJNJWXU896uC99xSBWpKoE2PRyybM= github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wXiMIu94LV/cH2Ek= +github.com/danielgtaylor/huma/v2 v2.29.0 h1:MPtwpWe6WWkklai1zpbapCxvwV2J2V2Tc3N2nNuUat0= +github.com/danielgtaylor/huma/v2 v2.29.0/go.mod h1:9BxJwkeoPPDEJ2Bg4yPwL1mM1rYpAwCAWFKoo723spk= +github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb h1:i2LxO2SfvVq9d6Cejj+A8dPFyWg9VzE/xJp3IlKSPuU= +github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb/go.mod h1:9BxJwkeoPPDEJ2Bg4yPwL1mM1rYpAwCAWFKoo723spk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -60,9 +64,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -119,6 +128,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -133,6 +144,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/database/repository/github_app.go b/internal/database/repository/github_app.go new file mode 100644 index 00000000..340ec4f7 --- /dev/null +++ b/internal/database/repository/github_app.go @@ -0,0 +1,23 @@ +package repository + +import ( + "context" + + "github.com/google/go-github/v69/github" + "github.com/unbindapp/unbind-api/ent" +) + +// GetGithubApp returns the GithubApp entity., ent.NotFoundError if not found.s +func (r *Repository) GetGithubApp(ctx context.Context) (*ent.GithubApp, error) { + return r.DB.GithubApp.Query().First(ctx) +} + +func (r *Repository) CreateGithubApp(ctx context.Context, app *github.AppConfig) (*ent.GithubApp, error) { + return r.DB.GithubApp.Create(). + SetGithubAppID(app.GetID()). + SetClientID(app.GetClientID()). + SetClientSecret(app.GetClientSecret()). + SetWebhookSecret(app.GetWebhookSecret()). + SetPrivateKey(app.GetPEM()). + Save(ctx) +} diff --git a/internal/github/github.go b/internal/github/github.go new file mode 100644 index 00000000..c8ac244f --- /dev/null +++ b/internal/github/github.go @@ -0,0 +1,43 @@ +package github + +import ( + "net/http" + "net/url" + "strings" + + "github.com/google/go-github/v69/github" + "github.com/unbindapp/unbind-api/config" +) + +type GithubClient struct { + cfg *config.Config + client *github.Client +} + +func NewGithubClient(cfg *config.Config) *GithubClient { + httpClient := &http.Client{} + var githubClient *github.Client + + // Check if we're using GitHub Enterprise + if cfg.GithubURL != "https://github.com" && cfg.GithubURL != "" { + // For GitHub Enterprise, we need to set the base URL for the API + baseURL := cfg.GithubURL + // Make sure the URL has a trailing slash for the github client + if !strings.HasSuffix(baseURL, "/") { + baseURL = baseURL + "/" + } + + apiURL, _ := url.Parse(baseURL + "api/v3/") + uploadURL, _ := url.Parse(baseURL + "api/uploads/") + + // Create a GitHub client with enterprise URLs + githubClient, _ = github.NewClient(httpClient).WithEnterpriseURLs(apiURL.String(), uploadURL.String()) + } else { + githubClient = github.NewClient(httpClient) + } + + return &GithubClient{ + cfg: cfg, + client: githubClient, + } +} diff --git a/internal/github/manifest.go b/internal/github/manifest.go new file mode 100644 index 00000000..9393fe8a --- /dev/null +++ b/internal/github/manifest.go @@ -0,0 +1,69 @@ +package github + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-github/v69/github" +) + +// GitHubAppManifest represents the structure for GitHub App manifest +type GitHubAppManifest struct { + Name string `json:"name"` + Description string `json:"description"` + URL string `json:"url"` + HookAttributes HookAttributes `json:"hook_attributes"` + RedirectURL string `json:"redirect_url"` + Public bool `json:"public"` + DefaultPermissions DefaultPermissions `json:"default_permissions"` + DefaultEvents []string `json:"default_events"` +} + +// HookAttributes contains webhook configuration +type HookAttributes struct { + URL string `json:"url"` + Secret string `json:"secret"` +} + +// DefaultPermissions contains permission settings +type DefaultPermissions struct { + Contents string `json:"contents"` + Issues string `json:"issues"` + Metadata string `json:"metadata"` +} + +// CreateAppManifest generates the GitHub App manifest +func (g *GithubClient) CreateAppManifest(redirectUrl string) *GitHubAppManifest { + return &GitHubAppManifest{ + Name: fmt.Sprintf("unbind-%s", g.cfg.UnbindSuffix), + Description: "Application to connect unbind with Github", + URL: g.cfg.ExternalURL, + HookAttributes: HookAttributes{ + URL: g.cfg.GithubWebhookURL, + }, + RedirectURL: redirectUrl, + Public: false, + DefaultPermissions: DefaultPermissions{ + Contents: "read", + Issues: "write", + Metadata: "read", + }, + DefaultEvents: []string{"push", "pull_request"}, + } +} + +// ManifestCodeConversion gets app configruation from github using the code +func (g *GithubClient) ManifestCodeConversion(ctx context.Context, code string) (*github.AppConfig, error) { + appConfig, response, err := g.client.Apps.CompleteAppManifest(ctx, code) + if err != nil { + return nil, fmt.Errorf("failed to exchange manifest code: %w", err) + } + + // Check for successful status code (201 Created) + if response.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + return appConfig, nil +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index ee2714ca..ebed8061 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -1,47 +1,46 @@ package middleware import ( - "context" "net/http" "strings" + "github.com/danielgtaylor/huma/v2" "github.com/unbindapp/unbind-api/internal/log" ) -func (m *Middleware) Authenticate(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Authorization header required", http.StatusUnauthorized) - return - } +func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { + authHeader := ctx.Header("Authorization") + if authHeader == "" { + huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header required") + return + } - bearerToken := strings.TrimPrefix(authHeader, "Bearer ") - token, err := m.verifier.Verify(r.Context(), bearerToken) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } + bearerToken := strings.TrimPrefix(authHeader, "Bearer ") + token, err := m.verifier.Verify(ctx.Context(), bearerToken) + if err != nil { + huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Invalid token") + return + } - var claims struct { - Email string `json:"email"` - Username string `json:"preferred_username"` - Subject string `json:"sub"` - } - if err := token.Claims(&claims); err != nil { - http.Error(w, "Failed to parse claims", http.StatusInternalServerError) - return - } + var claims struct { + Email string `json:"email"` + Username string `json:"preferred_username"` + Subject string `json:"sub"` + } + if err := token.Claims(&claims); err != nil { + log.Errorf("Failed to parse claims: %v", err) + huma.WriteErr(m.api, ctx, http.StatusInternalServerError, "Failed to parse claims") + return + } - // Get or create user using Ent - user, err := m.repository.GetOrCreateUser(r.Context(), claims.Email, claims.Username, claims.Subject) - if err != nil { - log.Errorf("Failed to process user: %v", err) - http.Error(w, "Failed to process user", http.StatusInternalServerError) - return - } + // Get or create user using Ent + user, err := m.repository.GetOrCreateUser(ctx.Context(), claims.Email, claims.Username, claims.Subject) + if err != nil { + log.Errorf("Failed to process user: %v", err) + huma.WriteErr(m.api, ctx, http.StatusInternalServerError, "Failed to process user") + return + } - ctx := context.WithValue(r.Context(), "user", user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) + ctx = huma.WithValue(ctx, "user", user) + next(ctx) } diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go index c67a2699..61466e0f 100644 --- a/internal/middleware/logger.go +++ b/internal/middleware/logger.go @@ -45,7 +45,7 @@ var ( // r.Use(middleware.Logger) // <--<< Logger should come before Recoverer // r.Use(middleware.Recoverer) // r.Get("/", handler) -func (m *Middleware) Logger(next http.Handler) http.Handler { +func Logger(next http.Handler) http.Handler { return DefaultLogger(next) } diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index a573941f..00fcdcb2 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/coreos/go-oidc/v3/oidc" + "github.com/danielgtaylor/huma/v2" "github.com/unbindapp/unbind-api/config" "github.com/unbindapp/unbind-api/internal/database/repository" ) @@ -12,9 +13,10 @@ import ( type Middleware struct { verifier *oidc.IDTokenVerifier repository repository.RepositoryInterface + api huma.API } -func NewMiddleware(cfg *config.Config, repository repository.RepositoryInterface) (*Middleware, error) { +func NewMiddleware(cfg *config.Config, repository repository.RepositoryInterface, api huma.API) (*Middleware, error) { provider, err := oidc.NewProvider(context.Background(), cfg.DexIssuerURL) if err != nil { return nil, fmt.Errorf("failed to get provider: %v", err) @@ -23,5 +25,6 @@ func NewMiddleware(cfg *config.Config, repository repository.RepositoryInterface return &Middleware{ verifier: provider.Verifier(&oidc.Config{ClientID: cfg.DexClientID}), repository: repository, + api: api, }, nil } diff --git a/internal/server/github.go b/internal/server/github.go new file mode 100644 index 00000000..a50c8ba4 --- /dev/null +++ b/internal/server/github.go @@ -0,0 +1,80 @@ +package server + +import ( + "context" + "fmt" + + "github.com/danielgtaylor/huma/v2" + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/internal/github" + "github.com/unbindapp/unbind-api/internal/log" +) + +type GithubCreateManifestInput struct { + Body struct { + RedirectURL string `json:"redirect_url"` + } +} + +type GithubCreateManifestResponse struct { + Body struct { + PostURL string `json:"post_url"` + Manifest *github.GitHubAppManifest `json:"manifest"` + } +} + +// Create a manifest that the user can use to create a GitHub app +func (s *Server) GithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { + // ! TODO - for now we only need 1 github app, so lets check uniqueness, in future we may want more for some reason + ghApp, err := s.Repository.GetGithubApp(ctx) + if ghApp != nil { + log.Info("Github app already exists") + return nil, huma.Error400BadRequest("Github app already exists") + } + if err != nil && !ent.IsNotFound(err) { + log.Error("Error getting github app", "err", err) + return nil, huma.Error500InternalServerError("Failed to get github app") + } + + // Create GitHub app manifest + manifest := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) + + // Create resp + resp := &GithubCreateManifestResponse{} + resp.Body.Manifest = manifest + resp.Body.PostURL = fmt.Sprintf("%s/settings/apps/new", s.Cfg.GithubURL) + return resp, nil +} + +// Connect the new github app to our instance, via manifest code exchange +type GithubAppConnectInput struct { + Body struct { + Code string `json:"code"` + } +} + +type GithubAppConnectResponse struct { + Body struct { + Name string `json:"name"` + } +} + +func (s *Server) GithubAppConnect(ctx context.Context, input *GithubAppConnectInput) (*GithubAppConnectResponse, error) { + // Exchange the code for tokens. + appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) + if err != nil { + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange manifest code: %v", err)) + } + + // Save the app config + ghApp, err := s.Repository.CreateGithubApp(ctx, appConfig) + if err != nil { + log.Error("Error saving github app", "err", err) + return nil, huma.Error500InternalServerError("Failed to save github app") + } + + // Return the app name + resp := &GithubAppConnectResponse{} + resp.Body.Name = ghApp.Name + return resp, nil +} diff --git a/internal/server/server.go b/internal/server/server.go index dd5515f5..efbeef2c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,8 @@ import ( "context" "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/internal/database/repository" + "github.com/unbindapp/unbind-api/internal/github" "github.com/unbindapp/unbind-api/internal/kubeclient" "golang.org/x/oauth2" ) @@ -13,9 +15,11 @@ type EmptyInput struct{} // Server implements generated.ServerInterface type Server struct { - KubeClient *kubeclient.KubeClient - Cfg *config.Config - OauthConfig *oauth2.Config + KubeClient *kubeclient.KubeClient + Cfg *config.Config + OauthConfig *oauth2.Config + GithubClient *github.GithubClient + Repository *repository.Repository } // HealthCheck is your /health endpoint diff --git a/internal/server/user.go b/internal/server/user.go new file mode 100644 index 00000000..3ab2092d --- /dev/null +++ b/internal/server/user.go @@ -0,0 +1,29 @@ +package server + +import ( + "context" + + "github.com/danielgtaylor/huma/v2" + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/internal/log" +) + +type MeResponse struct { + Body struct { + User *ent.User `json:"user"` + } +} + +// Me handles GET /me +func (s *Server) Me(ctx context.Context, _ *EmptyInput) (*MeResponse, error) { + + user, ok := ctx.Value("user").(*ent.User) + if !ok { + log.Error("Error getting user from context") + return nil, huma.Error500InternalServerError("Unable to retrieve user") + } + + resp := &MeResponse{} + resp.Body.User = user + return resp, nil +} diff --git a/internal/utils/url.go b/internal/utils/url.go new file mode 100644 index 00000000..254ac813 --- /dev/null +++ b/internal/utils/url.go @@ -0,0 +1,52 @@ +package utils + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +// ValidateAndExtractDomain takes a URL string and: +// 1. Validates if it's a proper URL (must start with http:// or https://) +// 2. Extracts the domain name +// 3. Converts special characters to hyphens +// Returns the transformed domain and an error if invalid +func ValidateAndExtractDomain(urlStr string) (string, error) { + // Parse the URL + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + // Check if scheme is http or https + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return "", fmt.Errorf("invalid URL scheme: %s", parsedURL.Scheme) + } + + // Get hostname and remove port if present + hostname := parsedURL.Hostname() + if hostname == "" { + return "", fmt.Errorf("missing hostname in URL") + } + + // Handle any potential URL encoding in the hostname + hostname, _ = url.QueryUnescape(hostname) + + // Convert special characters to hyphens and remove trailing/leading hyphens + transformed := transformDomain(hostname) + + return transformed, nil +} + +// transformDomain converts special characters to hyphens +func transformDomain(domain string) string { + // Replace dots and other special characters with hyphens + reg := regexp.MustCompile(`[^a-zA-Z0-9]+`) + transformed := reg.ReplaceAllString(domain, "-") + + // Remove leading and trailing hyphens + transformed = strings.Trim(transformed, "-") + + return transformed +} diff --git a/internal/utils/url_test.go b/internal/utils/url_test.go new file mode 100644 index 00000000..1a82870f --- /dev/null +++ b/internal/utils/url_test.go @@ -0,0 +1,194 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTransformDomain(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Simple domain", + input: "example", + expected: "example", + }, + { + name: "Domain with dots", + input: "example.com", + expected: "example-com", + }, + { + name: "Domain with special characters", + input: "hello.world!@#$%^&*", + expected: "hello-world", + }, + { + name: "Domain with multiple dots", + input: "sub.domain.example.com", + expected: "sub-domain-example-com", + }, + { + name: "Domain with leading and trailing special chars", + input: ".example.com.", + expected: "example-com", + }, + { + name: "Domain with consecutive special chars", + input: "example..com", + expected: "example-com", + }, + { + name: "Domain with hyphen", + input: "my-domain.com", + expected: "my-domain-com", + }, + { + name: "Domain with underscore", + input: "my_domain.com", + expected: "my-domain-com", + }, + { + name: "Empty string", + input: "", + expected: "", + }, + { + name: "Only special characters", + input: "....", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := transformDomain(tt.input) + assert.Equal(t, tt.expected, result, "They should be equal") + }) + } +} + +func TestValidateAndExtractDomain(t *testing.T) { + tests := []struct { + name string + input string + expectedResult string + expectError bool + errorContains string + }{ + { + name: "Valid HTTP URL with port", + input: "http://localhost:8099", + expectedResult: "localhost", + expectError: false, + }, + { + name: "Valid HTTPS URL", + input: "https://unbind.app", + expectedResult: "unbind-app", + expectError: false, + }, + { + name: "Invalid scheme", + input: "ht://localhost:8099", + expectedResult: "", + expectError: true, + errorContains: "invalid URL scheme", + }, + { + name: "Valid URL with subdomain", + input: "https://sub.example.com", + expectedResult: "sub-example-com", + expectError: false, + }, + { + name: "Valid URL with path", + input: "https://example.com/path/to/resource", + expectedResult: "example-com", + expectError: false, + }, + { + name: "Valid URL with query params", + input: "https://example.com?param=value", + expectedResult: "example-com", + expectError: false, + }, + { + name: "Valid URL with fragment", + input: "https://example.com#section", + expectedResult: "example-com", + expectError: false, + }, + { + name: "Valid URL with username and password", + input: "https://user:pass@example.com", + expectedResult: "example-com", + expectError: false, + }, + { + name: "Empty string", + input: "", + expectedResult: "", + expectError: true, + }, + { + name: "Invalid URL format", + input: "not a url", + expectedResult: "", + expectError: true, + }, + { + name: "Missing scheme", + input: "example.com", + expectedResult: "", + expectError: true, + errorContains: "invalid URL scheme", + }, + { + name: "FTP URL (invalid scheme)", + input: "ftp://example.com", + expectedResult: "", + expectError: true, + errorContains: "invalid URL scheme", + }, + { + name: "URL with IP address", + input: "http://192.168.1.1", + expectedResult: "192-168-1-1", + expectError: false, + }, + { + name: "URL with IPv6 address", + input: "http://[2001:db8::1]", + expectedResult: "2001-db8-1", + expectError: false, + }, + { + name: "URL with unusual characters in domain", + input: "https://example-site!.com", + expectedResult: "example-site-com", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ValidateAndExtractDomain(tt.input) + + // Check error expectation + if tt.expectError { + assert.Error(t, err, "Expected an error but got none") + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains, "Error message should contain expected text") + } + } else { + assert.NoError(t, err, "Did not expect an error") + assert.Equal(t, tt.expectedResult, result, "Results should match") + } + }) + } +} From 3875b70dc1370df0c04b7c195e33655e3ccf1e2f Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 25 Feb 2025 17:08:09 +0000 Subject: [PATCH 08/22] Add deployment files --- .github/actions/k8s-deploy/action.yml | 29 +++++++ .github/workflows/ci_master.yml | 104 ++++++++++++++++++++++++++ Dockerfile.ci | 8 ++ k8s/deployment.yaml | 49 ++++++++++++ k8s/ingress.yaml | 38 ++++++++++ k8s/kustomization.yaml | 7 ++ k8s/svc.yaml | 12 +++ 7 files changed, 247 insertions(+) create mode 100644 .github/actions/k8s-deploy/action.yml create mode 100644 .github/workflows/ci_master.yml create mode 100644 Dockerfile.ci create mode 100644 k8s/deployment.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/svc.yaml diff --git a/.github/actions/k8s-deploy/action.yml b/.github/actions/k8s-deploy/action.yml new file mode 100644 index 00000000..94284ecb --- /dev/null +++ b/.github/actions/k8s-deploy/action.yml @@ -0,0 +1,29 @@ +name: Deploy to Kubernetes +description: Deploy to Kubernetes +inputs: + image: + description: Image to deploy + required: true + kube_config: + description: base64 encoded kube config + required: true +runs: + using: composite + steps: + - uses: imranismail/setup-kustomize@v1 + with: + kustomize-version: "3.5.4" + + - name: Set image + working-directory: ./k8s + shell: bash + run: | + kustomize edit set image replaceme=${{ inputs.image }} + kustomize build . > ./ci.yaml + + - name: Deploy image to k8s cluster + uses: bbedward/kubectl@master + env: + KUBE_CONFIG_DATA: ${{ inputs.kube_config }} + with: + args: apply -f ./k8s/ci.yaml diff --git a/.github/workflows/ci_master.yml b/.github/workflows/ci_master.yml new file mode 100644 index 00000000..291dfc80 --- /dev/null +++ b/.github/workflows/ci_master.yml @@ -0,0 +1,104 @@ +name: 💫 CI + +on: + push: + branches: [master] + +concurrency: + group: environment-${{ github.ref }} + cancel-in-progress: true + +jobs: + setup_env: + name: ⚙️ Setup environment + runs-on: ubuntu-latest + steps: + - name: Add SHORT_SHA env property + run: echo "SHORT_SHA=`echo ${GITHUB_SHA::7}`" >> $GITHUB_ENV + + - name: Put commit msg in environment + run: echo "COMMIT_MSG=${{ github.event.head_commit.message }}" >> $GITHUB_ENV + + - name: Escape commit message + run: | + echo "COMMIT_MSG=$(echo $COMMIT_MSG | tr -d \'\\\")" >> $GITHUB_ENV + + - name: Get branch name (merge) + if: github.event_name != 'pull_request' + shell: bash + run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV + + - name: Get branch name (pull request) + if: github.event_name == 'pull_request' + shell: bash + run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV + + outputs: + short_sha: ${{ env.SHORT_SHA }} + commit_msg: ${{ env.COMMIT_MSG }} + branch_name: ${{ env.BRANCH_NAME }} + + build: + name: 🔨 Build Binaries and Docker Image + runs-on: ubuntu-latest + needs: setup_env + + env: + GITHUB_RUN_ID: ${{ github.run_id }} + steps: + - uses: actions/checkout@v3 + + - name: Set build start in env variable + run: echo "BUILD_START=$(date +%s)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Setup Go with cache + uses: actions/setup-go@v3 + with: + go-version-file: ./go.mod + + - name: Login to registry + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build GO Server Binary + run: | + cd cmd && GOARCH=arm64 go build -ldflags "-s -w -X main.Version=${{ needs.setup_env.outputs.short_sha }} -X \"main.CommitMsg=${{ needs.setup_env.outputs.commit_msg }}\" -X main.BuildStart=${{ env.BUILD_START }}" -o server && cd .. + + - name: Build and push image + if: success() + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/arm64 + push: true + file: ./Dockerfile.ci + tags: unbindapp/unbind-api:${{ needs.setup_env.outputs.branch_name }}-${{ env.GITHUB_RUN_ID }} + + deploy_prod: + name: 🚀 Deploy Apps (PROD) + runs-on: ubuntu-latest + needs: + - setup_env + - build + env: + GITHUB_RUN_ID: ${{ github.run_id }} + steps: + - uses: actions/checkout@v3 + + - name: Deploy + uses: ./.github/actions/k8s-deploy + with: + image: unbindapp/unbind-api:${{ needs.setup_env.outputs.branch_name}}-${{ env.GITHUB_RUN_ID }} + kube_config: ${{ secrets.K3S_KUBE_CONFIG }} diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 00000000..55261486 --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,8 @@ +# Docker file for github actions CI build +FROM stablecog/ubuntu:22.04 + +COPY ./cmd/server /app/server + +EXPOSE 8080 + +CMD ["/app/server"] \ No newline at end of file diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 00000000..3010096e --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,49 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: unbind-api-deployment + namespace: unbind + labels: + app: unbind-api +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app: unbind-api + template: + metadata: + labels: + app: unbind-api + spec: + containers: + - name: unbind-api + image: replaceme + resources: + requests: + cpu: 100m + memory: 200Mi + limits: + memory: 2Gi + cpu: 500m + ports: + - containerPort: 8089 + imagePullPolicy: "Always" + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: unbind-db-credentialsOpaque + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: unbind-db-credentialsOpaque + key: password + envFrom: + - secretRef: + name: unbind-api diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 00000000..9576cb0e --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,38 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: unbind-api-ingress + namespace: unbind + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/configuration-snippet: | + real_ip_header CF-Connecting-IP; + nginx.ingress.kubernetes.io/eventsource: "true" + nginx.ingress.kubernetes.io/add-base-url: "true" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/websocket-services: "unbind-api" + nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" + nginx.ingress.kubernetes.io/proxy-read-timeout: "21600" + nginx.ingress.kubernetes.io/proxy-body-size: 10m + nginx.ingress.kubernetes.io/upstream-hash-by: $realip_remote_addr + nginx.ingress.kubernetes.io/affinity: "cookie" + nginx.ingress.kubernetes.io/session-cookie-name: "unbind-api-server" + nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" +spec: + tls: + - hosts: + - api.unbind.app + secretName: api-unbind-app-tls + rules: + - host: api.unbind.app + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: unbind-api + port: + number: 8089 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 00000000..812cb8ff --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: unbind-system +resources: + - deployment.yaml + - svc.yaml + - ingress.yaml diff --git a/k8s/svc.yaml b/k8s/svc.yaml new file mode 100644 index 00000000..4220c251 --- /dev/null +++ b/k8s/svc.yaml @@ -0,0 +1,12 @@ +kind: Service +apiVersion: v1 +metadata: + name: unbind-api + namespace: unbind +spec: + selector: + app: unbind-api + type: ClusterIP + ports: + - port: 8089 + targetPort: 8089 From a337f35645189a4322ff9440ee555d1c2b8787d8 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 25 Feb 2025 17:11:27 +0000 Subject: [PATCH 09/22] Fix typo --- k8s/deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 3010096e..c143ced5 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -37,12 +37,12 @@ spec: - name: POSTGRES_USER valueFrom: secretKeyRef: - name: unbind-db-credentialsOpaque + name: unbind-db-credentials key: username - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: - name: unbind-db-credentialsOpaque + name: unbind-db-credentials key: password envFrom: - secretRef: From 9f4dbaa84800270a4ccf0370bc0579118819193a Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 25 Feb 2025 19:26:33 +0000 Subject: [PATCH 10/22] Use real IP address in log middleware --- internal/middleware/logger.go | 3 ++- internal/utils/net_headers.go | 17 +++++++++++++++ internal/utils/net_headers_test.go | 35 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 internal/utils/net_headers.go create mode 100644 internal/utils/net_headers_test.go diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go index 61466e0f..47df03fd 100644 --- a/internal/middleware/logger.go +++ b/internal/middleware/logger.go @@ -12,6 +12,7 @@ import ( "time" "github.com/unbindapp/unbind-api/internal/log" + "github.com/unbindapp/unbind-api/internal/utils" ) // for defining context keys was copied from Go 1.7's new use of context in net/http. @@ -120,7 +121,7 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry { cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto) entry.buf.WriteString("from ") - entry.buf.WriteString(r.RemoteAddr) + entry.buf.WriteString(utils.GetIPAddress(r)) entry.buf.WriteString(" - ") return entry diff --git a/internal/utils/net_headers.go b/internal/utils/net_headers.go new file mode 100644 index 00000000..c4ba53d6 --- /dev/null +++ b/internal/utils/net_headers.go @@ -0,0 +1,17 @@ +package utils + +import "net/http" + +func GetIPAddress(r *http.Request) string { + IPAddress := r.Header.Get("CF-Connecting-IP") + if IPAddress == "" { + IPAddress = r.Header.Get("X-Real-Ip") + } + if IPAddress == "" { + IPAddress = r.Header.Get("X-Forwarded-For") + } + if IPAddress == "" { + IPAddress = r.RemoteAddr + } + return IPAddress +} diff --git a/internal/utils/net_headers_test.go b/internal/utils/net_headers_test.go new file mode 100644 index 00000000..1511f48a --- /dev/null +++ b/internal/utils/net_headers_test.go @@ -0,0 +1,35 @@ +package utils + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetIPAddressFromHeader(t *testing.T) { + ip := "123.45.67.89" + + // 4 methods of getting IP Address, CF-Connecting-IP preferred, X-Real-Ip, then X-Forwarded-For, then RemoteAddr + + request, _ := http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) + request.Header.Set("CF-Connecting-IP", ip) + request.Header.Set("X-Real-Ip", "not-the-ip") + request.Header.Set("X-Forwarded-For", "not-the-ip") + assert.Equal(t, ip, GetIPAddress(request)) + + request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) + request.Header.Set("X-Real-Ip", ip) + request.Header.Set("X-Forwarded-For", "not-the-ip") + + assert.Equal(t, ip, GetIPAddress(request)) + + request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) + request.Header.Set("X-Forwarded-For", ip) + assert.Equal(t, ip, GetIPAddress(request)) + + request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) + request.RemoteAddr = ip + assert.Equal(t, ip, GetIPAddress(request)) +} From 0dbd664868cee880d057a814a48a5ba7af49b674 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 3 Mar 2025 20:32:57 +0000 Subject: [PATCH 11/22] Handling github installations, schema, github installation webhooks --- cmd/main.go | 4 + ent/client.go | 204 ++- ent/ent.go | 6 +- ent/githubapp.go | 56 +- ent/githubapp/githubapp.go | 44 +- ent/githubapp/where.go | 88 +- ent/githubapp_create.go | 148 +- ent/githubapp_delete.go | 2 +- ent/githubapp_query.go | 122 +- ent/githubapp_update.go | 221 ++- ent/githubinstallation.go | 267 ++++ ent/githubinstallation/githubinstallation.go | 220 +++ ent/githubinstallation/where.go | 484 ++++++ ent/githubinstallation_create.go | 1216 +++++++++++++++ ent/githubinstallation_delete.go | 88 ++ ent/githubinstallation_query.go | 629 ++++++++ ent/githubinstallation_update.go | 781 ++++++++++ ent/hook/hook.go | 12 + ent/migrate/schema.go | 42 +- ent/mutation.go | 1325 +++++++++++++++-- ent/predicate/predicate.go | 3 + ent/runtime.go | 48 +- ent/schema/github_app.go | 21 +- ent/schema/github_installation.go | 97 ++ ent/tx.go | 3 + internal/database/repository/github_app.go | 13 +- .../repository/github_installation.go | 71 + .../models/github_installation_metadata.go | 6 + internal/server/github.go | 52 + internal/server/github_webhook.go | 149 ++ 30 files changed, 5999 insertions(+), 423 deletions(-) create mode 100644 ent/githubinstallation.go create mode 100644 ent/githubinstallation/githubinstallation.go create mode 100644 ent/githubinstallation/where.go create mode 100644 ent/githubinstallation_create.go create mode 100644 ent/githubinstallation_delete.go create mode 100644 ent/githubinstallation_query.go create mode 100644 ent/githubinstallation_update.go create mode 100644 ent/schema/github_installation.go create mode 100644 internal/database/repository/github_installation.go create mode 100644 internal/models/github_installation_metadata.go create mode 100644 internal/server/github_webhook.go diff --git a/cmd/main.go b/cmd/main.go index 0b4ef86f..d204561f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -93,6 +93,10 @@ func main() { ghGroup.UseMiddleware(mw.Authenticate) huma.Post(ghGroup, "/app/manifest", srvImpl.GithubManifestCreate) huma.Post(ghGroup, "/app/connect", srvImpl.GithubAppConnect) + huma.Post(ghGroup, "/app/install/{app_id}", srvImpl.GithubAppInstall) + + webhookGroup := huma.NewGroup(api, "/webhook") + huma.Post(webhookGroup, "/github", srvImpl.HandleGithubWebhook) // ! // huma.Get(api, "/teams", srvImpl.ListTeams) diff --git a/ent/client.go b/ent/client.go index 1b977a7f..6ffabaff 100644 --- a/ent/client.go +++ b/ent/client.go @@ -15,7 +15,9 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/user" stdsql "database/sql" @@ -28,6 +30,8 @@ type Client struct { Schema *migrate.Schema // GithubApp is the client for interacting with the GithubApp builders. GithubApp *GithubAppClient + // GithubInstallation is the client for interacting with the GithubInstallation builders. + GithubInstallation *GithubInstallationClient // User is the client for interacting with the User builders. User *UserClient } @@ -42,6 +46,7 @@ func NewClient(opts ...Option) *Client { func (c *Client) init() { c.Schema = migrate.NewSchema(c.driver) c.GithubApp = NewGithubAppClient(c.config) + c.GithubInstallation = NewGithubInstallationClient(c.config) c.User = NewUserClient(c.config) } @@ -133,10 +138,11 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { cfg := c.config cfg.driver = tx return &Tx{ - ctx: ctx, - config: cfg, - GithubApp: NewGithubAppClient(cfg), - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + GithubApp: NewGithubAppClient(cfg), + GithubInstallation: NewGithubInstallationClient(cfg), + User: NewUserClient(cfg), }, nil } @@ -154,10 +160,11 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) cfg := c.config cfg.driver = &txDriver{tx: tx, drv: c.driver} return &Tx{ - ctx: ctx, - config: cfg, - GithubApp: NewGithubAppClient(cfg), - User: NewUserClient(cfg), + ctx: ctx, + config: cfg, + GithubApp: NewGithubAppClient(cfg), + GithubInstallation: NewGithubInstallationClient(cfg), + User: NewUserClient(cfg), }, nil } @@ -187,6 +194,7 @@ func (c *Client) Close() error { // In order to add hooks to a specific client, call: `client.Node.Use(...)`. func (c *Client) Use(hooks ...Hook) { c.GithubApp.Use(hooks...) + c.GithubInstallation.Use(hooks...) c.User.Use(hooks...) } @@ -194,6 +202,7 @@ func (c *Client) Use(hooks ...Hook) { // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. func (c *Client) Intercept(interceptors ...Interceptor) { c.GithubApp.Intercept(interceptors...) + c.GithubInstallation.Intercept(interceptors...) c.User.Intercept(interceptors...) } @@ -202,6 +211,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { switch m := m.(type) { case *GithubAppMutation: return c.GithubApp.mutate(ctx, m) + case *GithubInstallationMutation: + return c.GithubInstallation.mutate(ctx, m) case *UserMutation: return c.User.mutate(ctx, m) default: @@ -270,7 +281,7 @@ func (c *GithubAppClient) UpdateOne(ga *GithubApp) *GithubAppUpdateOne { } // UpdateOneID returns an update builder for the given id. -func (c *GithubAppClient) UpdateOneID(id uuid.UUID) *GithubAppUpdateOne { +func (c *GithubAppClient) UpdateOneID(id int64) *GithubAppUpdateOne { mutation := newGithubAppMutation(c.config, OpUpdateOne, withGithubAppID(id)) return &GithubAppUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} } @@ -287,7 +298,7 @@ func (c *GithubAppClient) DeleteOne(ga *GithubApp) *GithubAppDeleteOne { } // DeleteOneID returns a builder for deleting the given entity by its id. -func (c *GithubAppClient) DeleteOneID(id uuid.UUID) *GithubAppDeleteOne { +func (c *GithubAppClient) DeleteOneID(id int64) *GithubAppDeleteOne { builder := c.Delete().Where(githubapp.ID(id)) builder.mutation.id = &id builder.mutation.op = OpDeleteOne @@ -304,12 +315,12 @@ func (c *GithubAppClient) Query() *GithubAppQuery { } // Get returns a GithubApp entity by its id. -func (c *GithubAppClient) Get(ctx context.Context, id uuid.UUID) (*GithubApp, error) { +func (c *GithubAppClient) Get(ctx context.Context, id int64) (*GithubApp, error) { return c.Query().Where(githubapp.ID(id)).Only(ctx) } // GetX is like Get, but panics if an error occurs. -func (c *GithubAppClient) GetX(ctx context.Context, id uuid.UUID) *GithubApp { +func (c *GithubAppClient) GetX(ctx context.Context, id int64) *GithubApp { obj, err := c.Get(ctx, id) if err != nil { panic(err) @@ -317,6 +328,22 @@ func (c *GithubAppClient) GetX(ctx context.Context, id uuid.UUID) *GithubApp { return obj } +// QueryInstallations queries the installations edge of a GithubApp. +func (c *GithubAppClient) QueryInstallations(ga *GithubApp) *GithubInstallationQuery { + query := (&GithubInstallationClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := ga.ID + step := sqlgraph.NewStep( + sqlgraph.From(githubapp.Table, githubapp.FieldID, id), + sqlgraph.To(githubinstallation.Table, githubinstallation.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, githubapp.InstallationsTable, githubapp.InstallationsColumn), + ) + fromV = sqlgraph.Neighbors(ga.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *GithubAppClient) Hooks() []Hook { return c.hooks.GithubApp @@ -342,6 +369,155 @@ func (c *GithubAppClient) mutate(ctx context.Context, m *GithubAppMutation) (Val } } +// GithubInstallationClient is a client for the GithubInstallation schema. +type GithubInstallationClient struct { + config +} + +// NewGithubInstallationClient returns a client for the GithubInstallation from the given config. +func NewGithubInstallationClient(c config) *GithubInstallationClient { + return &GithubInstallationClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `githubinstallation.Hooks(f(g(h())))`. +func (c *GithubInstallationClient) Use(hooks ...Hook) { + c.hooks.GithubInstallation = append(c.hooks.GithubInstallation, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `githubinstallation.Intercept(f(g(h())))`. +func (c *GithubInstallationClient) Intercept(interceptors ...Interceptor) { + c.inters.GithubInstallation = append(c.inters.GithubInstallation, interceptors...) +} + +// Create returns a builder for creating a GithubInstallation entity. +func (c *GithubInstallationClient) Create() *GithubInstallationCreate { + mutation := newGithubInstallationMutation(c.config, OpCreate) + return &GithubInstallationCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of GithubInstallation entities. +func (c *GithubInstallationClient) CreateBulk(builders ...*GithubInstallationCreate) *GithubInstallationCreateBulk { + return &GithubInstallationCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *GithubInstallationClient) MapCreateBulk(slice any, setFunc func(*GithubInstallationCreate, int)) *GithubInstallationCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &GithubInstallationCreateBulk{err: fmt.Errorf("calling to GithubInstallationClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*GithubInstallationCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &GithubInstallationCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for GithubInstallation. +func (c *GithubInstallationClient) Update() *GithubInstallationUpdate { + mutation := newGithubInstallationMutation(c.config, OpUpdate) + return &GithubInstallationUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *GithubInstallationClient) UpdateOne(gi *GithubInstallation) *GithubInstallationUpdateOne { + mutation := newGithubInstallationMutation(c.config, OpUpdateOne, withGithubInstallation(gi)) + return &GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *GithubInstallationClient) UpdateOneID(id int64) *GithubInstallationUpdateOne { + mutation := newGithubInstallationMutation(c.config, OpUpdateOne, withGithubInstallationID(id)) + return &GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for GithubInstallation. +func (c *GithubInstallationClient) Delete() *GithubInstallationDelete { + mutation := newGithubInstallationMutation(c.config, OpDelete) + return &GithubInstallationDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *GithubInstallationClient) DeleteOne(gi *GithubInstallation) *GithubInstallationDeleteOne { + return c.DeleteOneID(gi.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *GithubInstallationClient) DeleteOneID(id int64) *GithubInstallationDeleteOne { + builder := c.Delete().Where(githubinstallation.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &GithubInstallationDeleteOne{builder} +} + +// Query returns a query builder for GithubInstallation. +func (c *GithubInstallationClient) Query() *GithubInstallationQuery { + return &GithubInstallationQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeGithubInstallation}, + inters: c.Interceptors(), + } +} + +// Get returns a GithubInstallation entity by its id. +func (c *GithubInstallationClient) Get(ctx context.Context, id int64) (*GithubInstallation, error) { + return c.Query().Where(githubinstallation.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *GithubInstallationClient) GetX(ctx context.Context, id int64) *GithubInstallation { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryGithubApps queries the github_apps edge of a GithubInstallation. +func (c *GithubInstallationClient) QueryGithubApps(gi *GithubInstallation) *GithubAppQuery { + query := (&GithubAppClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := gi.ID + step := sqlgraph.NewStep( + sqlgraph.From(githubinstallation.Table, githubinstallation.FieldID, id), + sqlgraph.To(githubapp.Table, githubapp.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, githubinstallation.GithubAppsTable, githubinstallation.GithubAppsColumn), + ) + fromV = sqlgraph.Neighbors(gi.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *GithubInstallationClient) Hooks() []Hook { + return c.hooks.GithubInstallation +} + +// Interceptors returns the client interceptors. +func (c *GithubInstallationClient) Interceptors() []Interceptor { + return c.inters.GithubInstallation +} + +func (c *GithubInstallationClient) mutate(ctx context.Context, m *GithubInstallationMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&GithubInstallationCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&GithubInstallationUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&GithubInstallationUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&GithubInstallationDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown GithubInstallation mutation op: %q", m.Op()) + } +} + // UserClient is a client for the User schema. type UserClient struct { config @@ -478,10 +654,10 @@ func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) // hooks and interceptors per client, for fast access. type ( hooks struct { - GithubApp, User []ent.Hook + GithubApp, GithubInstallation, User []ent.Hook } inters struct { - GithubApp, User []ent.Interceptor + GithubApp, GithubInstallation, User []ent.Interceptor } ) diff --git a/ent/ent.go b/ent/ent.go index 9728dda3..d855c46d 100644 --- a/ent/ent.go +++ b/ent/ent.go @@ -13,6 +13,7 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/user" ) @@ -74,8 +75,9 @@ var ( func checkColumn(table, column string) error { initCheck.Do(func() { columnCheck = sql.NewColumnCheck(map[string]func(string) bool{ - githubapp.Table: githubapp.ValidColumn, - user.Table: user.ValidColumn, + githubapp.Table: githubapp.ValidColumn, + githubinstallation.Table: githubinstallation.ValidColumn, + user.Table: user.ValidColumn, }) }) return columnCheck(table, column) diff --git a/ent/githubapp.go b/ent/githubapp.go index 8b201009..761fc8fa 100644 --- a/ent/githubapp.go +++ b/ent/githubapp.go @@ -9,7 +9,6 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" - "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent/githubapp" ) @@ -17,14 +16,12 @@ import ( type GithubApp struct { config `json:"-"` // ID of the ent. - // The primary key of the entity. - ID uuid.UUID `json:"id"` + // The GitHub App ID + ID int64 `json:"id,omitempty"` // The time at which the entity was created. CreatedAt time.Time `json:"created_at,omitempty"` // The time at which the entity was last updated. UpdatedAt time.Time `json:"updated_at,omitempty"` - // The GitHub App ID - GithubAppID int64 `json:"github_app_id,omitempty"` // Name of the GitHub App Name string `json:"name,omitempty"` // OAuth client ID of the GitHub App @@ -34,23 +31,42 @@ type GithubApp struct { // Webhook secret for GitHub events WebhookSecret string `json:"-"` // Private key (PEM) for GitHub App authentication - PrivateKey string `json:"-"` + PrivateKey string `json:"-"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the GithubAppQuery when eager-loading is set. + Edges GithubAppEdges `json:"edges"` selectValues sql.SelectValues } +// GithubAppEdges holds the relations/edges for other nodes in the graph. +type GithubAppEdges struct { + // Installations holds the value of the installations edge. + Installations []*GithubInstallation `json:"installations,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [1]bool +} + +// InstallationsOrErr returns the Installations value or an error if the edge +// was not loaded in eager-loading. +func (e GithubAppEdges) InstallationsOrErr() ([]*GithubInstallation, error) { + if e.loadedTypes[0] { + return e.Installations, nil + } + return nil, &NotLoadedError{edge: "installations"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*GithubApp) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case githubapp.FieldGithubAppID: + case githubapp.FieldID: values[i] = new(sql.NullInt64) case githubapp.FieldName, githubapp.FieldClientID, githubapp.FieldClientSecret, githubapp.FieldWebhookSecret, githubapp.FieldPrivateKey: values[i] = new(sql.NullString) case githubapp.FieldCreatedAt, githubapp.FieldUpdatedAt: values[i] = new(sql.NullTime) - case githubapp.FieldID: - values[i] = new(uuid.UUID) default: values[i] = new(sql.UnknownType) } @@ -67,11 +83,11 @@ func (ga *GithubApp) assignValues(columns []string, values []any) error { for i := range columns { switch columns[i] { case githubapp.FieldID: - if value, ok := values[i].(*uuid.UUID); !ok { - return fmt.Errorf("unexpected type %T for field id", values[i]) - } else if value != nil { - ga.ID = *value + value, ok := values[i].(*sql.NullInt64) + if !ok { + return fmt.Errorf("unexpected type %T for field id", value) } + ga.ID = int64(value.Int64) case githubapp.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -84,12 +100,6 @@ func (ga *GithubApp) assignValues(columns []string, values []any) error { } else if value.Valid { ga.UpdatedAt = value.Time } - case githubapp.FieldGithubAppID: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field github_app_id", values[i]) - } else if value.Valid { - ga.GithubAppID = value.Int64 - } case githubapp.FieldName: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field name", values[i]) @@ -133,6 +143,11 @@ func (ga *GithubApp) Value(name string) (ent.Value, error) { return ga.selectValues.Get(name) } +// QueryInstallations queries the "installations" edge of the GithubApp entity. +func (ga *GithubApp) QueryInstallations() *GithubInstallationQuery { + return NewGithubAppClient(ga.config).QueryInstallations(ga) +} + // Update returns a builder for updating this GithubApp. // Note that you need to call GithubApp.Unwrap() before calling this method if this GithubApp // was returned from a transaction, and the transaction was committed or rolled back. @@ -162,9 +177,6 @@ func (ga *GithubApp) String() string { builder.WriteString("updated_at=") builder.WriteString(ga.UpdatedAt.Format(time.ANSIC)) builder.WriteString(", ") - builder.WriteString("github_app_id=") - builder.WriteString(fmt.Sprintf("%v", ga.GithubAppID)) - builder.WriteString(", ") builder.WriteString("name=") builder.WriteString(ga.Name) builder.WriteString(", ") diff --git a/ent/githubapp/githubapp.go b/ent/githubapp/githubapp.go index bfa06bd8..487cc5c7 100644 --- a/ent/githubapp/githubapp.go +++ b/ent/githubapp/githubapp.go @@ -6,7 +6,7 @@ import ( "time" "entgo.io/ent/dialect/sql" - "github.com/google/uuid" + "entgo.io/ent/dialect/sql/sqlgraph" ) const ( @@ -18,8 +18,6 @@ const ( FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. FieldUpdatedAt = "updated_at" - // FieldGithubAppID holds the string denoting the github_app_id field in the database. - FieldGithubAppID = "github_app_id" // FieldName holds the string denoting the name field in the database. FieldName = "name" // FieldClientID holds the string denoting the client_id field in the database. @@ -30,8 +28,17 @@ const ( FieldWebhookSecret = "webhook_secret" // FieldPrivateKey holds the string denoting the private_key field in the database. FieldPrivateKey = "private_key" + // EdgeInstallations holds the string denoting the installations edge name in mutations. + EdgeInstallations = "installations" // Table holds the table name of the githubapp in the database. Table = "github_apps" + // InstallationsTable is the table that holds the installations relation/edge. + InstallationsTable = "github_installations" + // InstallationsInverseTable is the table name for the GithubInstallation entity. + // It exists in this package in order to avoid circular dependency with the "githubinstallation" package. + InstallationsInverseTable = "github_installations" + // InstallationsColumn is the table column denoting the installations relation/edge. + InstallationsColumn = "github_app_id" ) // Columns holds all SQL columns for githubapp fields. @@ -39,7 +46,6 @@ var Columns = []string{ FieldID, FieldCreatedAt, FieldUpdatedAt, - FieldGithubAppID, FieldName, FieldClientID, FieldClientSecret, @@ -66,8 +72,8 @@ var ( UpdateDefaultUpdatedAt func() time.Time // NameValidator is a validator for the "name" field. It is called by the builders before save. NameValidator func(string) error - // DefaultID holds the default value on creation for the "id" field. - DefaultID func() uuid.UUID + // IDValidator is a validator for the "id" field. It is called by the builders before save. + IDValidator func(int64) error ) // OrderOption defines the ordering options for the GithubApp queries. @@ -88,11 +94,6 @@ func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() } -// ByGithubAppID orders the results by the github_app_id field. -func ByGithubAppID(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldGithubAppID, opts...).ToFunc() -} - // ByName orders the results by the name field. func ByName(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldName, opts...).ToFunc() @@ -117,3 +118,24 @@ func ByWebhookSecret(opts ...sql.OrderTermOption) OrderOption { func ByPrivateKey(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldPrivateKey, opts...).ToFunc() } + +// ByInstallationsCount orders the results by installations count. +func ByInstallationsCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newInstallationsStep(), opts...) + } +} + +// ByInstallations orders the results by installations terms. +func ByInstallations(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newInstallationsStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} +func newInstallationsStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(InstallationsInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, InstallationsTable, InstallationsColumn), + ) +} diff --git a/ent/githubapp/where.go b/ent/githubapp/where.go index 050c781a..1cd2fb98 100644 --- a/ent/githubapp/where.go +++ b/ent/githubapp/where.go @@ -6,52 +6,52 @@ import ( "time" "entgo.io/ent/dialect/sql" - "github.com/google/uuid" + "entgo.io/ent/dialect/sql/sqlgraph" "github.com/unbindapp/unbind-api/ent/predicate" ) // ID filters vertices based on their ID field. -func ID(id uuid.UUID) predicate.GithubApp { +func ID(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldEQ(FieldID, id)) } // IDEQ applies the EQ predicate on the ID field. -func IDEQ(id uuid.UUID) predicate.GithubApp { +func IDEQ(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldEQ(FieldID, id)) } // IDNEQ applies the NEQ predicate on the ID field. -func IDNEQ(id uuid.UUID) predicate.GithubApp { +func IDNEQ(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldNEQ(FieldID, id)) } // IDIn applies the In predicate on the ID field. -func IDIn(ids ...uuid.UUID) predicate.GithubApp { +func IDIn(ids ...int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldIn(FieldID, ids...)) } // IDNotIn applies the NotIn predicate on the ID field. -func IDNotIn(ids ...uuid.UUID) predicate.GithubApp { +func IDNotIn(ids ...int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldNotIn(FieldID, ids...)) } // IDGT applies the GT predicate on the ID field. -func IDGT(id uuid.UUID) predicate.GithubApp { +func IDGT(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldGT(FieldID, id)) } // IDGTE applies the GTE predicate on the ID field. -func IDGTE(id uuid.UUID) predicate.GithubApp { +func IDGTE(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldGTE(FieldID, id)) } // IDLT applies the LT predicate on the ID field. -func IDLT(id uuid.UUID) predicate.GithubApp { +func IDLT(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldLT(FieldID, id)) } // IDLTE applies the LTE predicate on the ID field. -func IDLTE(id uuid.UUID) predicate.GithubApp { +func IDLTE(id int64) predicate.GithubApp { return predicate.GithubApp(sql.FieldLTE(FieldID, id)) } @@ -65,11 +65,6 @@ func UpdatedAt(v time.Time) predicate.GithubApp { return predicate.GithubApp(sql.FieldEQ(FieldUpdatedAt, v)) } -// GithubAppID applies equality check predicate on the "github_app_id" field. It's identical to GithubAppIDEQ. -func GithubAppID(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldEQ(FieldGithubAppID, v)) -} - // Name applies equality check predicate on the "name" field. It's identical to NameEQ. func Name(v string) predicate.GithubApp { return predicate.GithubApp(sql.FieldEQ(FieldName, v)) @@ -175,46 +170,6 @@ func UpdatedAtLTE(v time.Time) predicate.GithubApp { return predicate.GithubApp(sql.FieldLTE(FieldUpdatedAt, v)) } -// GithubAppIDEQ applies the EQ predicate on the "github_app_id" field. -func GithubAppIDEQ(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldEQ(FieldGithubAppID, v)) -} - -// GithubAppIDNEQ applies the NEQ predicate on the "github_app_id" field. -func GithubAppIDNEQ(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldNEQ(FieldGithubAppID, v)) -} - -// GithubAppIDIn applies the In predicate on the "github_app_id" field. -func GithubAppIDIn(vs ...int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldIn(FieldGithubAppID, vs...)) -} - -// GithubAppIDNotIn applies the NotIn predicate on the "github_app_id" field. -func GithubAppIDNotIn(vs ...int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldNotIn(FieldGithubAppID, vs...)) -} - -// GithubAppIDGT applies the GT predicate on the "github_app_id" field. -func GithubAppIDGT(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldGT(FieldGithubAppID, v)) -} - -// GithubAppIDGTE applies the GTE predicate on the "github_app_id" field. -func GithubAppIDGTE(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldGTE(FieldGithubAppID, v)) -} - -// GithubAppIDLT applies the LT predicate on the "github_app_id" field. -func GithubAppIDLT(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldLT(FieldGithubAppID, v)) -} - -// GithubAppIDLTE applies the LTE predicate on the "github_app_id" field. -func GithubAppIDLTE(v int64) predicate.GithubApp { - return predicate.GithubApp(sql.FieldLTE(FieldGithubAppID, v)) -} - // NameEQ applies the EQ predicate on the "name" field. func NameEQ(v string) predicate.GithubApp { return predicate.GithubApp(sql.FieldEQ(FieldName, v)) @@ -540,6 +495,29 @@ func PrivateKeyContainsFold(v string) predicate.GithubApp { return predicate.GithubApp(sql.FieldContainsFold(FieldPrivateKey, v)) } +// HasInstallations applies the HasEdge predicate on the "installations" edge. +func HasInstallations() predicate.GithubApp { + return predicate.GithubApp(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, InstallationsTable, InstallationsColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasInstallationsWith applies the HasEdge predicate on the "installations" edge with a given conditions (other predicates). +func HasInstallationsWith(preds ...predicate.GithubInstallation) predicate.GithubApp { + return predicate.GithubApp(func(s *sql.Selector) { + step := newInstallationsStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.GithubApp) predicate.GithubApp { return predicate.GithubApp(sql.AndPredicates(predicates...)) diff --git a/ent/githubapp_create.go b/ent/githubapp_create.go index dda4e42f..30eea879 100644 --- a/ent/githubapp_create.go +++ b/ent/githubapp_create.go @@ -8,12 +8,11 @@ import ( "fmt" "time" - "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" - "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" ) // GithubAppCreate is the builder for creating a GithubApp entity. @@ -52,12 +51,6 @@ func (gac *GithubAppCreate) SetNillableUpdatedAt(t *time.Time) *GithubAppCreate return gac } -// SetGithubAppID sets the "github_app_id" field. -func (gac *GithubAppCreate) SetGithubAppID(i int64) *GithubAppCreate { - gac.mutation.SetGithubAppID(i) - return gac -} - // SetName sets the "name" field. func (gac *GithubAppCreate) SetName(s string) *GithubAppCreate { gac.mutation.SetName(s) @@ -89,19 +82,26 @@ func (gac *GithubAppCreate) SetPrivateKey(s string) *GithubAppCreate { } // SetID sets the "id" field. -func (gac *GithubAppCreate) SetID(u uuid.UUID) *GithubAppCreate { - gac.mutation.SetID(u) +func (gac *GithubAppCreate) SetID(i int64) *GithubAppCreate { + gac.mutation.SetID(i) return gac } -// SetNillableID sets the "id" field if the given value is not nil. -func (gac *GithubAppCreate) SetNillableID(u *uuid.UUID) *GithubAppCreate { - if u != nil { - gac.SetID(*u) - } +// AddInstallationIDs adds the "installations" edge to the GithubInstallation entity by IDs. +func (gac *GithubAppCreate) AddInstallationIDs(ids ...int64) *GithubAppCreate { + gac.mutation.AddInstallationIDs(ids...) return gac } +// AddInstallations adds the "installations" edges to the GithubInstallation entity. +func (gac *GithubAppCreate) AddInstallations(g ...*GithubInstallation) *GithubAppCreate { + ids := make([]int64, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return gac.AddInstallationIDs(ids...) +} + // Mutation returns the GithubAppMutation object of the builder. func (gac *GithubAppCreate) Mutation() *GithubAppMutation { return gac.mutation @@ -145,10 +145,6 @@ func (gac *GithubAppCreate) defaults() { v := githubapp.DefaultUpdatedAt() gac.mutation.SetUpdatedAt(v) } - if _, ok := gac.mutation.ID(); !ok { - v := githubapp.DefaultID() - gac.mutation.SetID(v) - } } // check runs all checks and user-defined validators on the builder. @@ -159,9 +155,6 @@ func (gac *GithubAppCreate) check() error { if _, ok := gac.mutation.UpdatedAt(); !ok { return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "GithubApp.updated_at"`)} } - if _, ok := gac.mutation.GithubAppID(); !ok { - return &ValidationError{Name: "github_app_id", err: errors.New(`ent: missing required field "GithubApp.github_app_id"`)} - } if _, ok := gac.mutation.Name(); !ok { return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "GithubApp.name"`)} } @@ -182,6 +175,11 @@ func (gac *GithubAppCreate) check() error { if _, ok := gac.mutation.PrivateKey(); !ok { return &ValidationError{Name: "private_key", err: errors.New(`ent: missing required field "GithubApp.private_key"`)} } + if v, ok := gac.mutation.ID(); ok { + if err := githubapp.IDValidator(v); err != nil { + return &ValidationError{Name: "id", err: fmt.Errorf(`ent: validator failed for field "GithubApp.id": %w`, err)} + } + } return nil } @@ -196,12 +194,9 @@ func (gac *GithubAppCreate) sqlSave(ctx context.Context) (*GithubApp, error) { } return nil, err } - if _spec.ID.Value != nil { - if id, ok := _spec.ID.Value.(*uuid.UUID); ok { - _node.ID = *id - } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { - return nil, err - } + if _spec.ID.Value != _node.ID { + id := _spec.ID.Value.(int64) + _node.ID = int64(id) } gac.mutation.id = &_node.ID gac.mutation.done = true @@ -211,12 +206,12 @@ func (gac *GithubAppCreate) sqlSave(ctx context.Context) (*GithubApp, error) { func (gac *GithubAppCreate) createSpec() (*GithubApp, *sqlgraph.CreateSpec) { var ( _node = &GithubApp{config: gac.config} - _spec = sqlgraph.NewCreateSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec = sqlgraph.NewCreateSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64)) ) _spec.OnConflict = gac.conflict if id, ok := gac.mutation.ID(); ok { _node.ID = id - _spec.ID.Value = &id + _spec.ID.Value = id } if value, ok := gac.mutation.CreatedAt(); ok { _spec.SetField(githubapp.FieldCreatedAt, field.TypeTime, value) @@ -226,10 +221,6 @@ func (gac *GithubAppCreate) createSpec() (*GithubApp, *sqlgraph.CreateSpec) { _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) _node.UpdatedAt = value } - if value, ok := gac.mutation.GithubAppID(); ok { - _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) - _node.GithubAppID = value - } if value, ok := gac.mutation.Name(); ok { _spec.SetField(githubapp.FieldName, field.TypeString, value) _node.Name = value @@ -250,6 +241,22 @@ func (gac *GithubAppCreate) createSpec() (*GithubApp, *sqlgraph.CreateSpec) { _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) _node.PrivateKey = value } + if nodes := gac.mutation.InstallationsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } @@ -314,24 +321,6 @@ func (u *GithubAppUpsert) UpdateUpdatedAt() *GithubAppUpsert { return u } -// SetGithubAppID sets the "github_app_id" field. -func (u *GithubAppUpsert) SetGithubAppID(v int64) *GithubAppUpsert { - u.Set(githubapp.FieldGithubAppID, v) - return u -} - -// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. -func (u *GithubAppUpsert) UpdateGithubAppID() *GithubAppUpsert { - u.SetExcluded(githubapp.FieldGithubAppID) - return u -} - -// AddGithubAppID adds v to the "github_app_id" field. -func (u *GithubAppUpsert) AddGithubAppID(v int64) *GithubAppUpsert { - u.Add(githubapp.FieldGithubAppID, v) - return u -} - // SetName sets the "name" field. func (u *GithubAppUpsert) SetName(v string) *GithubAppUpsert { u.Set(githubapp.FieldName, v) @@ -457,27 +446,6 @@ func (u *GithubAppUpsertOne) UpdateUpdatedAt() *GithubAppUpsertOne { }) } -// SetGithubAppID sets the "github_app_id" field. -func (u *GithubAppUpsertOne) SetGithubAppID(v int64) *GithubAppUpsertOne { - return u.Update(func(s *GithubAppUpsert) { - s.SetGithubAppID(v) - }) -} - -// AddGithubAppID adds v to the "github_app_id" field. -func (u *GithubAppUpsertOne) AddGithubAppID(v int64) *GithubAppUpsertOne { - return u.Update(func(s *GithubAppUpsert) { - s.AddGithubAppID(v) - }) -} - -// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. -func (u *GithubAppUpsertOne) UpdateGithubAppID() *GithubAppUpsertOne { - return u.Update(func(s *GithubAppUpsert) { - s.UpdateGithubAppID() - }) -} - // SetName sets the "name" field. func (u *GithubAppUpsertOne) SetName(v string) *GithubAppUpsertOne { return u.Update(func(s *GithubAppUpsert) { @@ -564,12 +532,7 @@ func (u *GithubAppUpsertOne) ExecX(ctx context.Context) { } // Exec executes the UPSERT query and returns the inserted/updated ID. -func (u *GithubAppUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { - if u.create.driver.Dialect() == dialect.MySQL { - // In case of "ON CONFLICT", there is no way to get back non-numeric ID - // fields from the database since MySQL does not support the RETURNING clause. - return id, errors.New("ent: GithubAppUpsertOne.ID is not supported by MySQL driver. Use GithubAppUpsertOne.Exec instead") - } +func (u *GithubAppUpsertOne) ID(ctx context.Context) (id int64, err error) { node, err := u.create.Save(ctx) if err != nil { return id, err @@ -578,7 +541,7 @@ func (u *GithubAppUpsertOne) ID(ctx context.Context) (id uuid.UUID, err error) { } // IDX is like ID, but panics if an error occurs. -func (u *GithubAppUpsertOne) IDX(ctx context.Context) uuid.UUID { +func (u *GithubAppUpsertOne) IDX(ctx context.Context) int64 { id, err := u.ID(ctx) if err != nil { panic(err) @@ -633,6 +596,10 @@ func (gacb *GithubAppCreateBulk) Save(ctx context.Context) ([]*GithubApp, error) return nil, err } mutation.id = &nodes[i].ID + if specs[i].ID.Value != nil && nodes[i].ID == 0 { + id := specs[i].ID.Value.(int64) + nodes[i].ID = int64(id) + } mutation.done = true return nodes[i], nil }) @@ -780,27 +747,6 @@ func (u *GithubAppUpsertBulk) UpdateUpdatedAt() *GithubAppUpsertBulk { }) } -// SetGithubAppID sets the "github_app_id" field. -func (u *GithubAppUpsertBulk) SetGithubAppID(v int64) *GithubAppUpsertBulk { - return u.Update(func(s *GithubAppUpsert) { - s.SetGithubAppID(v) - }) -} - -// AddGithubAppID adds v to the "github_app_id" field. -func (u *GithubAppUpsertBulk) AddGithubAppID(v int64) *GithubAppUpsertBulk { - return u.Update(func(s *GithubAppUpsert) { - s.AddGithubAppID(v) - }) -} - -// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. -func (u *GithubAppUpsertBulk) UpdateGithubAppID() *GithubAppUpsertBulk { - return u.Update(func(s *GithubAppUpsert) { - s.UpdateGithubAppID() - }) -} - // SetName sets the "name" field. func (u *GithubAppUpsertBulk) SetName(v string) *GithubAppUpsertBulk { return u.Update(func(s *GithubAppUpsert) { diff --git a/ent/githubapp_delete.go b/ent/githubapp_delete.go index 39a87110..884dc4f7 100644 --- a/ent/githubapp_delete.go +++ b/ent/githubapp_delete.go @@ -40,7 +40,7 @@ func (gad *GithubAppDelete) ExecX(ctx context.Context) int { } func (gad *GithubAppDelete) sqlExec(ctx context.Context) (int, error) { - _spec := sqlgraph.NewDeleteSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec := sqlgraph.NewDeleteSpec(githubapp.Table, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64)) if ps := gad.mutation.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { for i := range ps { diff --git a/ent/githubapp_query.go b/ent/githubapp_query.go index 94e9d158..55fa6c97 100644 --- a/ent/githubapp_query.go +++ b/ent/githubapp_query.go @@ -4,6 +4,7 @@ package ent import ( "context" + "database/sql/driver" "fmt" "math" @@ -11,19 +12,20 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" - "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/predicate" ) // GithubAppQuery is the builder for querying GithubApp entities. type GithubAppQuery struct { config - ctx *QueryContext - order []githubapp.OrderOption - inters []Interceptor - predicates []predicate.GithubApp - modifiers []func(*sql.Selector) + ctx *QueryContext + order []githubapp.OrderOption + inters []Interceptor + predicates []predicate.GithubApp + withInstallations *GithubInstallationQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -60,6 +62,28 @@ func (gaq *GithubAppQuery) Order(o ...githubapp.OrderOption) *GithubAppQuery { return gaq } +// QueryInstallations chains the current query on the "installations" edge. +func (gaq *GithubAppQuery) QueryInstallations() *GithubInstallationQuery { + query := (&GithubInstallationClient{config: gaq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := gaq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := gaq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(githubapp.Table, githubapp.FieldID, selector), + sqlgraph.To(githubinstallation.Table, githubinstallation.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, githubapp.InstallationsTable, githubapp.InstallationsColumn), + ) + fromU = sqlgraph.SetNeighbors(gaq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first GithubApp entity from the query. // Returns a *NotFoundError when no GithubApp was found. func (gaq *GithubAppQuery) First(ctx context.Context) (*GithubApp, error) { @@ -84,8 +108,8 @@ func (gaq *GithubAppQuery) FirstX(ctx context.Context) *GithubApp { // FirstID returns the first GithubApp ID from the query. // Returns a *NotFoundError when no GithubApp ID was found. -func (gaq *GithubAppQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { - var ids []uuid.UUID +func (gaq *GithubAppQuery) FirstID(ctx context.Context) (id int64, err error) { + var ids []int64 if ids, err = gaq.Limit(1).IDs(setContextOp(ctx, gaq.ctx, ent.OpQueryFirstID)); err != nil { return } @@ -97,7 +121,7 @@ func (gaq *GithubAppQuery) FirstID(ctx context.Context) (id uuid.UUID, err error } // FirstIDX is like FirstID, but panics if an error occurs. -func (gaq *GithubAppQuery) FirstIDX(ctx context.Context) uuid.UUID { +func (gaq *GithubAppQuery) FirstIDX(ctx context.Context) int64 { id, err := gaq.FirstID(ctx) if err != nil && !IsNotFound(err) { panic(err) @@ -135,8 +159,8 @@ func (gaq *GithubAppQuery) OnlyX(ctx context.Context) *GithubApp { // OnlyID is like Only, but returns the only GithubApp ID in the query. // Returns a *NotSingularError when more than one GithubApp ID is found. // Returns a *NotFoundError when no entities are found. -func (gaq *GithubAppQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { - var ids []uuid.UUID +func (gaq *GithubAppQuery) OnlyID(ctx context.Context) (id int64, err error) { + var ids []int64 if ids, err = gaq.Limit(2).IDs(setContextOp(ctx, gaq.ctx, ent.OpQueryOnlyID)); err != nil { return } @@ -152,7 +176,7 @@ func (gaq *GithubAppQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) } // OnlyIDX is like OnlyID, but panics if an error occurs. -func (gaq *GithubAppQuery) OnlyIDX(ctx context.Context) uuid.UUID { +func (gaq *GithubAppQuery) OnlyIDX(ctx context.Context) int64 { id, err := gaq.OnlyID(ctx) if err != nil { panic(err) @@ -180,7 +204,7 @@ func (gaq *GithubAppQuery) AllX(ctx context.Context) []*GithubApp { } // IDs executes the query and returns a list of GithubApp IDs. -func (gaq *GithubAppQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { +func (gaq *GithubAppQuery) IDs(ctx context.Context) (ids []int64, err error) { if gaq.ctx.Unique == nil && gaq.path != nil { gaq.Unique(true) } @@ -192,7 +216,7 @@ func (gaq *GithubAppQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) } // IDsX is like IDs, but panics if an error occurs. -func (gaq *GithubAppQuery) IDsX(ctx context.Context) []uuid.UUID { +func (gaq *GithubAppQuery) IDsX(ctx context.Context) []int64 { ids, err := gaq.IDs(ctx) if err != nil { panic(err) @@ -247,11 +271,12 @@ func (gaq *GithubAppQuery) Clone() *GithubAppQuery { return nil } return &GithubAppQuery{ - config: gaq.config, - ctx: gaq.ctx.Clone(), - order: append([]githubapp.OrderOption{}, gaq.order...), - inters: append([]Interceptor{}, gaq.inters...), - predicates: append([]predicate.GithubApp{}, gaq.predicates...), + config: gaq.config, + ctx: gaq.ctx.Clone(), + order: append([]githubapp.OrderOption{}, gaq.order...), + inters: append([]Interceptor{}, gaq.inters...), + predicates: append([]predicate.GithubApp{}, gaq.predicates...), + withInstallations: gaq.withInstallations.Clone(), // clone intermediate query. sql: gaq.sql.Clone(), path: gaq.path, @@ -259,6 +284,17 @@ func (gaq *GithubAppQuery) Clone() *GithubAppQuery { } } +// WithInstallations tells the query-builder to eager-load the nodes that are connected to +// the "installations" edge. The optional arguments are used to configure the query builder of the edge. +func (gaq *GithubAppQuery) WithInstallations(opts ...func(*GithubInstallationQuery)) *GithubAppQuery { + query := (&GithubInstallationClient{config: gaq.config}).Query() + for _, opt := range opts { + opt(query) + } + gaq.withInstallations = query + return gaq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -335,8 +371,11 @@ func (gaq *GithubAppQuery) prepareQuery(ctx context.Context) error { func (gaq *GithubAppQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*GithubApp, error) { var ( - nodes = []*GithubApp{} - _spec = gaq.querySpec() + nodes = []*GithubApp{} + _spec = gaq.querySpec() + loadedTypes = [1]bool{ + gaq.withInstallations != nil, + } ) _spec.ScanValues = func(columns []string) ([]any, error) { return (*GithubApp).scanValues(nil, columns) @@ -344,6 +383,7 @@ func (gaq *GithubAppQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*G _spec.Assign = func(columns []string, values []any) error { node := &GithubApp{config: gaq.config} nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } if len(gaq.modifiers) > 0 { @@ -358,9 +398,47 @@ func (gaq *GithubAppQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*G if len(nodes) == 0 { return nodes, nil } + if query := gaq.withInstallations; query != nil { + if err := gaq.loadInstallations(ctx, query, nodes, + func(n *GithubApp) { n.Edges.Installations = []*GithubInstallation{} }, + func(n *GithubApp, e *GithubInstallation) { n.Edges.Installations = append(n.Edges.Installations, e) }); err != nil { + return nil, err + } + } return nodes, nil } +func (gaq *GithubAppQuery) loadInstallations(ctx context.Context, query *GithubInstallationQuery, nodes []*GithubApp, init func(*GithubApp), assign func(*GithubApp, *GithubInstallation)) error { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[int64]*GithubApp) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + if init != nil { + init(nodes[i]) + } + } + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(githubinstallation.FieldGithubAppID) + } + query.Where(predicate.GithubInstallation(func(s *sql.Selector) { + s.Where(sql.InValues(s.C(githubapp.InstallationsColumn), fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + fk := n.GithubAppID + node, ok := nodeids[fk] + if !ok { + return fmt.Errorf(`unexpected referenced foreign-key "github_app_id" returned %v for node %v`, fk, n.ID) + } + assign(node, n) + } + return nil +} + func (gaq *GithubAppQuery) sqlCount(ctx context.Context) (int, error) { _spec := gaq.querySpec() if len(gaq.modifiers) > 0 { @@ -374,7 +452,7 @@ func (gaq *GithubAppQuery) sqlCount(ctx context.Context) (int, error) { } func (gaq *GithubAppQuery) querySpec() *sqlgraph.QuerySpec { - _spec := sqlgraph.NewQuerySpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec := sqlgraph.NewQuerySpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64)) _spec.From = gaq.sql if unique := gaq.ctx.Unique; unique != nil { _spec.Unique = *unique diff --git a/ent/githubapp_update.go b/ent/githubapp_update.go index 7e51bf3d..dd92dd3a 100644 --- a/ent/githubapp_update.go +++ b/ent/githubapp_update.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/predicate" ) @@ -35,27 +36,6 @@ func (gau *GithubAppUpdate) SetUpdatedAt(t time.Time) *GithubAppUpdate { return gau } -// SetGithubAppID sets the "github_app_id" field. -func (gau *GithubAppUpdate) SetGithubAppID(i int64) *GithubAppUpdate { - gau.mutation.ResetGithubAppID() - gau.mutation.SetGithubAppID(i) - return gau -} - -// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. -func (gau *GithubAppUpdate) SetNillableGithubAppID(i *int64) *GithubAppUpdate { - if i != nil { - gau.SetGithubAppID(*i) - } - return gau -} - -// AddGithubAppID adds i to the "github_app_id" field. -func (gau *GithubAppUpdate) AddGithubAppID(i int64) *GithubAppUpdate { - gau.mutation.AddGithubAppID(i) - return gau -} - // SetName sets the "name" field. func (gau *GithubAppUpdate) SetName(s string) *GithubAppUpdate { gau.mutation.SetName(s) @@ -126,11 +106,47 @@ func (gau *GithubAppUpdate) SetNillablePrivateKey(s *string) *GithubAppUpdate { return gau } +// AddInstallationIDs adds the "installations" edge to the GithubInstallation entity by IDs. +func (gau *GithubAppUpdate) AddInstallationIDs(ids ...int64) *GithubAppUpdate { + gau.mutation.AddInstallationIDs(ids...) + return gau +} + +// AddInstallations adds the "installations" edges to the GithubInstallation entity. +func (gau *GithubAppUpdate) AddInstallations(g ...*GithubInstallation) *GithubAppUpdate { + ids := make([]int64, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return gau.AddInstallationIDs(ids...) +} + // Mutation returns the GithubAppMutation object of the builder. func (gau *GithubAppUpdate) Mutation() *GithubAppMutation { return gau.mutation } +// ClearInstallations clears all "installations" edges to the GithubInstallation entity. +func (gau *GithubAppUpdate) ClearInstallations() *GithubAppUpdate { + gau.mutation.ClearInstallations() + return gau +} + +// RemoveInstallationIDs removes the "installations" edge to GithubInstallation entities by IDs. +func (gau *GithubAppUpdate) RemoveInstallationIDs(ids ...int64) *GithubAppUpdate { + gau.mutation.RemoveInstallationIDs(ids...) + return gau +} + +// RemoveInstallations removes "installations" edges to GithubInstallation entities. +func (gau *GithubAppUpdate) RemoveInstallations(g ...*GithubInstallation) *GithubAppUpdate { + ids := make([]int64, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return gau.RemoveInstallationIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (gau *GithubAppUpdate) Save(ctx context.Context) (int, error) { gau.defaults() @@ -187,7 +203,7 @@ func (gau *GithubAppUpdate) sqlSave(ctx context.Context) (n int, err error) { if err := gau.check(); err != nil { return n, err } - _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64)) if ps := gau.mutation.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { for i := range ps { @@ -198,12 +214,6 @@ func (gau *GithubAppUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := gau.mutation.UpdatedAt(); ok { _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) } - if value, ok := gau.mutation.GithubAppID(); ok { - _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) - } - if value, ok := gau.mutation.AddedGithubAppID(); ok { - _spec.AddField(githubapp.FieldGithubAppID, field.TypeInt64, value) - } if value, ok := gau.mutation.Name(); ok { _spec.SetField(githubapp.FieldName, field.TypeString, value) } @@ -219,6 +229,51 @@ func (gau *GithubAppUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := gau.mutation.PrivateKey(); ok { _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) } + if gau.mutation.InstallationsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gau.mutation.RemovedInstallationsIDs(); len(nodes) > 0 && !gau.mutation.InstallationsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gau.mutation.InstallationsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _spec.AddModifiers(gau.modifiers...) if n, err = sqlgraph.UpdateNodes(ctx, gau.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -247,27 +302,6 @@ func (gauo *GithubAppUpdateOne) SetUpdatedAt(t time.Time) *GithubAppUpdateOne { return gauo } -// SetGithubAppID sets the "github_app_id" field. -func (gauo *GithubAppUpdateOne) SetGithubAppID(i int64) *GithubAppUpdateOne { - gauo.mutation.ResetGithubAppID() - gauo.mutation.SetGithubAppID(i) - return gauo -} - -// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. -func (gauo *GithubAppUpdateOne) SetNillableGithubAppID(i *int64) *GithubAppUpdateOne { - if i != nil { - gauo.SetGithubAppID(*i) - } - return gauo -} - -// AddGithubAppID adds i to the "github_app_id" field. -func (gauo *GithubAppUpdateOne) AddGithubAppID(i int64) *GithubAppUpdateOne { - gauo.mutation.AddGithubAppID(i) - return gauo -} - // SetName sets the "name" field. func (gauo *GithubAppUpdateOne) SetName(s string) *GithubAppUpdateOne { gauo.mutation.SetName(s) @@ -338,11 +372,47 @@ func (gauo *GithubAppUpdateOne) SetNillablePrivateKey(s *string) *GithubAppUpdat return gauo } +// AddInstallationIDs adds the "installations" edge to the GithubInstallation entity by IDs. +func (gauo *GithubAppUpdateOne) AddInstallationIDs(ids ...int64) *GithubAppUpdateOne { + gauo.mutation.AddInstallationIDs(ids...) + return gauo +} + +// AddInstallations adds the "installations" edges to the GithubInstallation entity. +func (gauo *GithubAppUpdateOne) AddInstallations(g ...*GithubInstallation) *GithubAppUpdateOne { + ids := make([]int64, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return gauo.AddInstallationIDs(ids...) +} + // Mutation returns the GithubAppMutation object of the builder. func (gauo *GithubAppUpdateOne) Mutation() *GithubAppMutation { return gauo.mutation } +// ClearInstallations clears all "installations" edges to the GithubInstallation entity. +func (gauo *GithubAppUpdateOne) ClearInstallations() *GithubAppUpdateOne { + gauo.mutation.ClearInstallations() + return gauo +} + +// RemoveInstallationIDs removes the "installations" edge to GithubInstallation entities by IDs. +func (gauo *GithubAppUpdateOne) RemoveInstallationIDs(ids ...int64) *GithubAppUpdateOne { + gauo.mutation.RemoveInstallationIDs(ids...) + return gauo +} + +// RemoveInstallations removes "installations" edges to GithubInstallation entities. +func (gauo *GithubAppUpdateOne) RemoveInstallations(g ...*GithubInstallation) *GithubAppUpdateOne { + ids := make([]int64, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return gauo.RemoveInstallationIDs(ids...) +} + // Where appends a list predicates to the GithubAppUpdate builder. func (gauo *GithubAppUpdateOne) Where(ps ...predicate.GithubApp) *GithubAppUpdateOne { gauo.mutation.Where(ps...) @@ -412,7 +482,7 @@ func (gauo *GithubAppUpdateOne) sqlSave(ctx context.Context) (_node *GithubApp, if err := gauo.check(); err != nil { return _node, err } - _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeUUID)) + _spec := sqlgraph.NewUpdateSpec(githubapp.Table, githubapp.Columns, sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64)) id, ok := gauo.mutation.ID() if !ok { return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "GithubApp.id" for update`)} @@ -440,12 +510,6 @@ func (gauo *GithubAppUpdateOne) sqlSave(ctx context.Context) (_node *GithubApp, if value, ok := gauo.mutation.UpdatedAt(); ok { _spec.SetField(githubapp.FieldUpdatedAt, field.TypeTime, value) } - if value, ok := gauo.mutation.GithubAppID(); ok { - _spec.SetField(githubapp.FieldGithubAppID, field.TypeInt64, value) - } - if value, ok := gauo.mutation.AddedGithubAppID(); ok { - _spec.AddField(githubapp.FieldGithubAppID, field.TypeInt64, value) - } if value, ok := gauo.mutation.Name(); ok { _spec.SetField(githubapp.FieldName, field.TypeString, value) } @@ -461,6 +525,51 @@ func (gauo *GithubAppUpdateOne) sqlSave(ctx context.Context) (_node *GithubApp, if value, ok := gauo.mutation.PrivateKey(); ok { _spec.SetField(githubapp.FieldPrivateKey, field.TypeString, value) } + if gauo.mutation.InstallationsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gauo.mutation.RemovedInstallationsIDs(); len(nodes) > 0 && !gauo.mutation.InstallationsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gauo.mutation.InstallationsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: githubapp.InstallationsTable, + Columns: []string{githubapp.InstallationsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _spec.AddModifiers(gauo.modifiers...) _node = &GithubApp{config: gauo.config} _spec.Assign = _node.assignValues diff --git a/ent/githubinstallation.go b/ent/githubinstallation.go new file mode 100644 index 00000000..e00ca1fd --- /dev/null +++ b/ent/githubinstallation.go @@ -0,0 +1,267 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/internal/models" +) + +// GithubInstallation is the model entity for the GithubInstallation schema. +type GithubInstallation struct { + config `json:"-"` + // ID of the ent. + // The GitHub Installation ID + ID int64 `json:"id,omitempty"` + // The time at which the entity was created. + CreatedAt time.Time `json:"created_at,omitempty"` + // The time at which the entity was last updated. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // The GitHub App ID this installation belongs to + GithubAppID int64 `json:"github_app_id,omitempty"` + // The GitHub account ID (org or user) + AccountID int64 `json:"account_id,omitempty"` + // The GitHub account login (org or user name) + AccountLogin string `json:"account_login,omitempty"` + // Type of GitHub account + AccountType githubinstallation.AccountType `json:"account_type,omitempty"` + // The HTML URL to the GitHub account + AccountURL string `json:"account_url,omitempty"` + // Whether the installation has access to all repos or only selected ones + RepositorySelection githubinstallation.RepositorySelection `json:"repository_selection,omitempty"` + // Whether the installation is suspended + Suspended bool `json:"suspended,omitempty"` + // Whether the installation is active + Active bool `json:"active,omitempty"` + // Permissions granted to this installation + Permissions models.GithubInstallationPermissions `json:"permissions,omitempty"` + // Events this installation subscribes to + Events []string `json:"events,omitempty"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the GithubInstallationQuery when eager-loading is set. + Edges GithubInstallationEdges `json:"edges"` + selectValues sql.SelectValues +} + +// GithubInstallationEdges holds the relations/edges for other nodes in the graph. +type GithubInstallationEdges struct { + // GithubApps holds the value of the github_apps edge. + GithubApps *GithubApp `json:"github_apps,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [1]bool +} + +// GithubAppsOrErr returns the GithubApps value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e GithubInstallationEdges) GithubAppsOrErr() (*GithubApp, error) { + if e.GithubApps != nil { + return e.GithubApps, nil + } else if e.loadedTypes[0] { + return nil, &NotFoundError{label: githubapp.Label} + } + return nil, &NotLoadedError{edge: "github_apps"} +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*GithubInstallation) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case githubinstallation.FieldPermissions, githubinstallation.FieldEvents: + values[i] = new([]byte) + case githubinstallation.FieldSuspended, githubinstallation.FieldActive: + values[i] = new(sql.NullBool) + case githubinstallation.FieldID, githubinstallation.FieldGithubAppID, githubinstallation.FieldAccountID: + values[i] = new(sql.NullInt64) + case githubinstallation.FieldAccountLogin, githubinstallation.FieldAccountType, githubinstallation.FieldAccountURL, githubinstallation.FieldRepositorySelection: + values[i] = new(sql.NullString) + case githubinstallation.FieldCreatedAt, githubinstallation.FieldUpdatedAt: + values[i] = new(sql.NullTime) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the GithubInstallation fields. +func (gi *GithubInstallation) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case githubinstallation.FieldID: + value, ok := values[i].(*sql.NullInt64) + if !ok { + return fmt.Errorf("unexpected type %T for field id", value) + } + gi.ID = int64(value.Int64) + case githubinstallation.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + gi.CreatedAt = value.Time + } + case githubinstallation.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + gi.UpdatedAt = value.Time + } + case githubinstallation.FieldGithubAppID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field github_app_id", values[i]) + } else if value.Valid { + gi.GithubAppID = value.Int64 + } + case githubinstallation.FieldAccountID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field account_id", values[i]) + } else if value.Valid { + gi.AccountID = value.Int64 + } + case githubinstallation.FieldAccountLogin: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field account_login", values[i]) + } else if value.Valid { + gi.AccountLogin = value.String + } + case githubinstallation.FieldAccountType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field account_type", values[i]) + } else if value.Valid { + gi.AccountType = githubinstallation.AccountType(value.String) + } + case githubinstallation.FieldAccountURL: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field account_url", values[i]) + } else if value.Valid { + gi.AccountURL = value.String + } + case githubinstallation.FieldRepositorySelection: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field repository_selection", values[i]) + } else if value.Valid { + gi.RepositorySelection = githubinstallation.RepositorySelection(value.String) + } + case githubinstallation.FieldSuspended: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field suspended", values[i]) + } else if value.Valid { + gi.Suspended = value.Bool + } + case githubinstallation.FieldActive: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field active", values[i]) + } else if value.Valid { + gi.Active = value.Bool + } + case githubinstallation.FieldPermissions: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field permissions", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &gi.Permissions); err != nil { + return fmt.Errorf("unmarshal field permissions: %w", err) + } + } + case githubinstallation.FieldEvents: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field events", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &gi.Events); err != nil { + return fmt.Errorf("unmarshal field events: %w", err) + } + } + default: + gi.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the GithubInstallation. +// This includes values selected through modifiers, order, etc. +func (gi *GithubInstallation) Value(name string) (ent.Value, error) { + return gi.selectValues.Get(name) +} + +// QueryGithubApps queries the "github_apps" edge of the GithubInstallation entity. +func (gi *GithubInstallation) QueryGithubApps() *GithubAppQuery { + return NewGithubInstallationClient(gi.config).QueryGithubApps(gi) +} + +// Update returns a builder for updating this GithubInstallation. +// Note that you need to call GithubInstallation.Unwrap() before calling this method if this GithubInstallation +// was returned from a transaction, and the transaction was committed or rolled back. +func (gi *GithubInstallation) Update() *GithubInstallationUpdateOne { + return NewGithubInstallationClient(gi.config).UpdateOne(gi) +} + +// Unwrap unwraps the GithubInstallation entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (gi *GithubInstallation) Unwrap() *GithubInstallation { + _tx, ok := gi.config.driver.(*txDriver) + if !ok { + panic("ent: GithubInstallation is not a transactional entity") + } + gi.config.driver = _tx.drv + return gi +} + +// String implements the fmt.Stringer. +func (gi *GithubInstallation) String() string { + var builder strings.Builder + builder.WriteString("GithubInstallation(") + builder.WriteString(fmt.Sprintf("id=%v, ", gi.ID)) + builder.WriteString("created_at=") + builder.WriteString(gi.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(gi.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("github_app_id=") + builder.WriteString(fmt.Sprintf("%v", gi.GithubAppID)) + builder.WriteString(", ") + builder.WriteString("account_id=") + builder.WriteString(fmt.Sprintf("%v", gi.AccountID)) + builder.WriteString(", ") + builder.WriteString("account_login=") + builder.WriteString(gi.AccountLogin) + builder.WriteString(", ") + builder.WriteString("account_type=") + builder.WriteString(fmt.Sprintf("%v", gi.AccountType)) + builder.WriteString(", ") + builder.WriteString("account_url=") + builder.WriteString(gi.AccountURL) + builder.WriteString(", ") + builder.WriteString("repository_selection=") + builder.WriteString(fmt.Sprintf("%v", gi.RepositorySelection)) + builder.WriteString(", ") + builder.WriteString("suspended=") + builder.WriteString(fmt.Sprintf("%v", gi.Suspended)) + builder.WriteString(", ") + builder.WriteString("active=") + builder.WriteString(fmt.Sprintf("%v", gi.Active)) + builder.WriteString(", ") + builder.WriteString("permissions=") + builder.WriteString(fmt.Sprintf("%v", gi.Permissions)) + builder.WriteString(", ") + builder.WriteString("events=") + builder.WriteString(fmt.Sprintf("%v", gi.Events)) + builder.WriteByte(')') + return builder.String() +} + +// GithubInstallations is a parsable slice of GithubInstallation. +type GithubInstallations []*GithubInstallation diff --git a/ent/githubinstallation/githubinstallation.go b/ent/githubinstallation/githubinstallation.go new file mode 100644 index 00000000..1aa5309d --- /dev/null +++ b/ent/githubinstallation/githubinstallation.go @@ -0,0 +1,220 @@ +// Code generated by ent, DO NOT EDIT. + +package githubinstallation + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" +) + +const ( + // Label holds the string label denoting the githubinstallation type in the database. + Label = "github_installation" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldGithubAppID holds the string denoting the github_app_id field in the database. + FieldGithubAppID = "github_app_id" + // FieldAccountID holds the string denoting the account_id field in the database. + FieldAccountID = "account_id" + // FieldAccountLogin holds the string denoting the account_login field in the database. + FieldAccountLogin = "account_login" + // FieldAccountType holds the string denoting the account_type field in the database. + FieldAccountType = "account_type" + // FieldAccountURL holds the string denoting the account_url field in the database. + FieldAccountURL = "account_url" + // FieldRepositorySelection holds the string denoting the repository_selection field in the database. + FieldRepositorySelection = "repository_selection" + // FieldSuspended holds the string denoting the suspended field in the database. + FieldSuspended = "suspended" + // FieldActive holds the string denoting the active field in the database. + FieldActive = "active" + // FieldPermissions holds the string denoting the permissions field in the database. + FieldPermissions = "permissions" + // FieldEvents holds the string denoting the events field in the database. + FieldEvents = "events" + // EdgeGithubApps holds the string denoting the github_apps edge name in mutations. + EdgeGithubApps = "github_apps" + // Table holds the table name of the githubinstallation in the database. + Table = "github_installations" + // GithubAppsTable is the table that holds the github_apps relation/edge. + GithubAppsTable = "github_installations" + // GithubAppsInverseTable is the table name for the GithubApp entity. + // It exists in this package in order to avoid circular dependency with the "githubapp" package. + GithubAppsInverseTable = "github_apps" + // GithubAppsColumn is the table column denoting the github_apps relation/edge. + GithubAppsColumn = "github_app_id" +) + +// Columns holds all SQL columns for githubinstallation fields. +var Columns = []string{ + FieldID, + FieldCreatedAt, + FieldUpdatedAt, + FieldGithubAppID, + FieldAccountID, + FieldAccountLogin, + FieldAccountType, + FieldAccountURL, + FieldRepositorySelection, + FieldSuspended, + FieldActive, + FieldPermissions, + FieldEvents, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // AccountLoginValidator is a validator for the "account_login" field. It is called by the builders before save. + AccountLoginValidator func(string) error + // AccountURLValidator is a validator for the "account_url" field. It is called by the builders before save. + AccountURLValidator func(string) error + // DefaultSuspended holds the default value on creation for the "suspended" field. + DefaultSuspended bool + // DefaultActive holds the default value on creation for the "active" field. + DefaultActive bool + // IDValidator is a validator for the "id" field. It is called by the builders before save. + IDValidator func(int64) error +) + +// AccountType defines the type for the "account_type" enum field. +type AccountType string + +// AccountType values. +const ( + AccountTypeOrganization AccountType = "Organization" + AccountTypeUser AccountType = "User" +) + +func (at AccountType) String() string { + return string(at) +} + +// AccountTypeValidator is a validator for the "account_type" field enum values. It is called by the builders before save. +func AccountTypeValidator(at AccountType) error { + switch at { + case AccountTypeOrganization, AccountTypeUser: + return nil + default: + return fmt.Errorf("githubinstallation: invalid enum value for account_type field: %q", at) + } +} + +// RepositorySelection defines the type for the "repository_selection" enum field. +type RepositorySelection string + +// RepositorySelectionAll is the default value of the RepositorySelection enum. +const DefaultRepositorySelection = RepositorySelectionAll + +// RepositorySelection values. +const ( + RepositorySelectionAll RepositorySelection = "all" + RepositorySelectionSelected RepositorySelection = "selected" +) + +func (rs RepositorySelection) String() string { + return string(rs) +} + +// RepositorySelectionValidator is a validator for the "repository_selection" field enum values. It is called by the builders before save. +func RepositorySelectionValidator(rs RepositorySelection) error { + switch rs { + case RepositorySelectionAll, RepositorySelectionSelected: + return nil + default: + return fmt.Errorf("githubinstallation: invalid enum value for repository_selection field: %q", rs) + } +} + +// OrderOption defines the ordering options for the GithubInstallation queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByGithubAppID orders the results by the github_app_id field. +func ByGithubAppID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldGithubAppID, opts...).ToFunc() +} + +// ByAccountID orders the results by the account_id field. +func ByAccountID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountID, opts...).ToFunc() +} + +// ByAccountLogin orders the results by the account_login field. +func ByAccountLogin(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountLogin, opts...).ToFunc() +} + +// ByAccountType orders the results by the account_type field. +func ByAccountType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountType, opts...).ToFunc() +} + +// ByAccountURL orders the results by the account_url field. +func ByAccountURL(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAccountURL, opts...).ToFunc() +} + +// ByRepositorySelection orders the results by the repository_selection field. +func ByRepositorySelection(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRepositorySelection, opts...).ToFunc() +} + +// BySuspended orders the results by the suspended field. +func BySuspended(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSuspended, opts...).ToFunc() +} + +// ByActive orders the results by the active field. +func ByActive(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldActive, opts...).ToFunc() +} + +// ByGithubAppsField orders the results by github_apps field. +func ByGithubAppsField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newGithubAppsStep(), sql.OrderByField(field, opts...)) + } +} +func newGithubAppsStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(GithubAppsInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, GithubAppsTable, GithubAppsColumn), + ) +} diff --git a/ent/githubinstallation/where.go b/ent/githubinstallation/where.go new file mode 100644 index 00000000..df3e3be3 --- /dev/null +++ b/ent/githubinstallation/where.go @@ -0,0 +1,484 @@ +// Code generated by ent, DO NOT EDIT. + +package githubinstallation + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldID, id)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// GithubAppID applies equality check predicate on the "github_app_id" field. It's identical to GithubAppIDEQ. +func GithubAppID(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldGithubAppID, v)) +} + +// AccountID applies equality check predicate on the "account_id" field. It's identical to AccountIDEQ. +func AccountID(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountID, v)) +} + +// AccountLogin applies equality check predicate on the "account_login" field. It's identical to AccountLoginEQ. +func AccountLogin(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountLogin, v)) +} + +// AccountURL applies equality check predicate on the "account_url" field. It's identical to AccountURLEQ. +func AccountURL(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountURL, v)) +} + +// Suspended applies equality check predicate on the "suspended" field. It's identical to SuspendedEQ. +func Suspended(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldSuspended, v)) +} + +// Active applies equality check predicate on the "active" field. It's identical to ActiveEQ. +func Active(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldActive, v)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// GithubAppIDEQ applies the EQ predicate on the "github_app_id" field. +func GithubAppIDEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldGithubAppID, v)) +} + +// GithubAppIDNEQ applies the NEQ predicate on the "github_app_id" field. +func GithubAppIDNEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldGithubAppID, v)) +} + +// GithubAppIDIn applies the In predicate on the "github_app_id" field. +func GithubAppIDIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldGithubAppID, vs...)) +} + +// GithubAppIDNotIn applies the NotIn predicate on the "github_app_id" field. +func GithubAppIDNotIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldGithubAppID, vs...)) +} + +// AccountIDEQ applies the EQ predicate on the "account_id" field. +func AccountIDEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountID, v)) +} + +// AccountIDNEQ applies the NEQ predicate on the "account_id" field. +func AccountIDNEQ(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountID, v)) +} + +// AccountIDIn applies the In predicate on the "account_id" field. +func AccountIDIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountID, vs...)) +} + +// AccountIDNotIn applies the NotIn predicate on the "account_id" field. +func AccountIDNotIn(vs ...int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountID, vs...)) +} + +// AccountIDGT applies the GT predicate on the "account_id" field. +func AccountIDGT(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAccountID, v)) +} + +// AccountIDGTE applies the GTE predicate on the "account_id" field. +func AccountIDGTE(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAccountID, v)) +} + +// AccountIDLT applies the LT predicate on the "account_id" field. +func AccountIDLT(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAccountID, v)) +} + +// AccountIDLTE applies the LTE predicate on the "account_id" field. +func AccountIDLTE(v int64) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAccountID, v)) +} + +// AccountLoginEQ applies the EQ predicate on the "account_login" field. +func AccountLoginEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountLogin, v)) +} + +// AccountLoginNEQ applies the NEQ predicate on the "account_login" field. +func AccountLoginNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountLogin, v)) +} + +// AccountLoginIn applies the In predicate on the "account_login" field. +func AccountLoginIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountLogin, vs...)) +} + +// AccountLoginNotIn applies the NotIn predicate on the "account_login" field. +func AccountLoginNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountLogin, vs...)) +} + +// AccountLoginGT applies the GT predicate on the "account_login" field. +func AccountLoginGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAccountLogin, v)) +} + +// AccountLoginGTE applies the GTE predicate on the "account_login" field. +func AccountLoginGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAccountLogin, v)) +} + +// AccountLoginLT applies the LT predicate on the "account_login" field. +func AccountLoginLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAccountLogin, v)) +} + +// AccountLoginLTE applies the LTE predicate on the "account_login" field. +func AccountLoginLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAccountLogin, v)) +} + +// AccountLoginContains applies the Contains predicate on the "account_login" field. +func AccountLoginContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldAccountLogin, v)) +} + +// AccountLoginHasPrefix applies the HasPrefix predicate on the "account_login" field. +func AccountLoginHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldAccountLogin, v)) +} + +// AccountLoginHasSuffix applies the HasSuffix predicate on the "account_login" field. +func AccountLoginHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldAccountLogin, v)) +} + +// AccountLoginEqualFold applies the EqualFold predicate on the "account_login" field. +func AccountLoginEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldAccountLogin, v)) +} + +// AccountLoginContainsFold applies the ContainsFold predicate on the "account_login" field. +func AccountLoginContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldAccountLogin, v)) +} + +// AccountTypeEQ applies the EQ predicate on the "account_type" field. +func AccountTypeEQ(v AccountType) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountType, v)) +} + +// AccountTypeNEQ applies the NEQ predicate on the "account_type" field. +func AccountTypeNEQ(v AccountType) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountType, v)) +} + +// AccountTypeIn applies the In predicate on the "account_type" field. +func AccountTypeIn(vs ...AccountType) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountType, vs...)) +} + +// AccountTypeNotIn applies the NotIn predicate on the "account_type" field. +func AccountTypeNotIn(vs ...AccountType) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountType, vs...)) +} + +// AccountURLEQ applies the EQ predicate on the "account_url" field. +func AccountURLEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldAccountURL, v)) +} + +// AccountURLNEQ applies the NEQ predicate on the "account_url" field. +func AccountURLNEQ(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldAccountURL, v)) +} + +// AccountURLIn applies the In predicate on the "account_url" field. +func AccountURLIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldAccountURL, vs...)) +} + +// AccountURLNotIn applies the NotIn predicate on the "account_url" field. +func AccountURLNotIn(vs ...string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldAccountURL, vs...)) +} + +// AccountURLGT applies the GT predicate on the "account_url" field. +func AccountURLGT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGT(FieldAccountURL, v)) +} + +// AccountURLGTE applies the GTE predicate on the "account_url" field. +func AccountURLGTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldGTE(FieldAccountURL, v)) +} + +// AccountURLLT applies the LT predicate on the "account_url" field. +func AccountURLLT(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLT(FieldAccountURL, v)) +} + +// AccountURLLTE applies the LTE predicate on the "account_url" field. +func AccountURLLTE(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldLTE(FieldAccountURL, v)) +} + +// AccountURLContains applies the Contains predicate on the "account_url" field. +func AccountURLContains(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContains(FieldAccountURL, v)) +} + +// AccountURLHasPrefix applies the HasPrefix predicate on the "account_url" field. +func AccountURLHasPrefix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasPrefix(FieldAccountURL, v)) +} + +// AccountURLHasSuffix applies the HasSuffix predicate on the "account_url" field. +func AccountURLHasSuffix(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldHasSuffix(FieldAccountURL, v)) +} + +// AccountURLEqualFold applies the EqualFold predicate on the "account_url" field. +func AccountURLEqualFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEqualFold(FieldAccountURL, v)) +} + +// AccountURLContainsFold applies the ContainsFold predicate on the "account_url" field. +func AccountURLContainsFold(v string) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldContainsFold(FieldAccountURL, v)) +} + +// RepositorySelectionEQ applies the EQ predicate on the "repository_selection" field. +func RepositorySelectionEQ(v RepositorySelection) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldRepositorySelection, v)) +} + +// RepositorySelectionNEQ applies the NEQ predicate on the "repository_selection" field. +func RepositorySelectionNEQ(v RepositorySelection) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldRepositorySelection, v)) +} + +// RepositorySelectionIn applies the In predicate on the "repository_selection" field. +func RepositorySelectionIn(vs ...RepositorySelection) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIn(FieldRepositorySelection, vs...)) +} + +// RepositorySelectionNotIn applies the NotIn predicate on the "repository_selection" field. +func RepositorySelectionNotIn(vs ...RepositorySelection) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotIn(FieldRepositorySelection, vs...)) +} + +// SuspendedEQ applies the EQ predicate on the "suspended" field. +func SuspendedEQ(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldSuspended, v)) +} + +// SuspendedNEQ applies the NEQ predicate on the "suspended" field. +func SuspendedNEQ(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldSuspended, v)) +} + +// ActiveEQ applies the EQ predicate on the "active" field. +func ActiveEQ(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldEQ(FieldActive, v)) +} + +// ActiveNEQ applies the NEQ predicate on the "active" field. +func ActiveNEQ(v bool) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNEQ(FieldActive, v)) +} + +// PermissionsIsNil applies the IsNil predicate on the "permissions" field. +func PermissionsIsNil() predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIsNull(FieldPermissions)) +} + +// PermissionsNotNil applies the NotNil predicate on the "permissions" field. +func PermissionsNotNil() predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotNull(FieldPermissions)) +} + +// EventsIsNil applies the IsNil predicate on the "events" field. +func EventsIsNil() predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldIsNull(FieldEvents)) +} + +// EventsNotNil applies the NotNil predicate on the "events" field. +func EventsNotNil() predicate.GithubInstallation { + return predicate.GithubInstallation(sql.FieldNotNull(FieldEvents)) +} + +// HasGithubApps applies the HasEdge predicate on the "github_apps" edge. +func HasGithubApps() predicate.GithubInstallation { + return predicate.GithubInstallation(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, GithubAppsTable, GithubAppsColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasGithubAppsWith applies the HasEdge predicate on the "github_apps" edge with a given conditions (other predicates). +func HasGithubAppsWith(preds ...predicate.GithubApp) predicate.GithubInstallation { + return predicate.GithubInstallation(func(s *sql.Selector) { + step := newGithubAppsStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.GithubInstallation) predicate.GithubInstallation { + return predicate.GithubInstallation(sql.NotPredicates(p)) +} diff --git a/ent/githubinstallation_create.go b/ent/githubinstallation_create.go new file mode 100644 index 00000000..06e50894 --- /dev/null +++ b/ent/githubinstallation_create.go @@ -0,0 +1,1216 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/internal/models" +) + +// GithubInstallationCreate is the builder for creating a GithubInstallation entity. +type GithubInstallationCreate struct { + config + mutation *GithubInstallationMutation + hooks []Hook + conflict []sql.ConflictOption +} + +// SetCreatedAt sets the "created_at" field. +func (gic *GithubInstallationCreate) SetCreatedAt(t time.Time) *GithubInstallationCreate { + gic.mutation.SetCreatedAt(t) + return gic +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillableCreatedAt(t *time.Time) *GithubInstallationCreate { + if t != nil { + gic.SetCreatedAt(*t) + } + return gic +} + +// SetUpdatedAt sets the "updated_at" field. +func (gic *GithubInstallationCreate) SetUpdatedAt(t time.Time) *GithubInstallationCreate { + gic.mutation.SetUpdatedAt(t) + return gic +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillableUpdatedAt(t *time.Time) *GithubInstallationCreate { + if t != nil { + gic.SetUpdatedAt(*t) + } + return gic +} + +// SetGithubAppID sets the "github_app_id" field. +func (gic *GithubInstallationCreate) SetGithubAppID(i int64) *GithubInstallationCreate { + gic.mutation.SetGithubAppID(i) + return gic +} + +// SetAccountID sets the "account_id" field. +func (gic *GithubInstallationCreate) SetAccountID(i int64) *GithubInstallationCreate { + gic.mutation.SetAccountID(i) + return gic +} + +// SetAccountLogin sets the "account_login" field. +func (gic *GithubInstallationCreate) SetAccountLogin(s string) *GithubInstallationCreate { + gic.mutation.SetAccountLogin(s) + return gic +} + +// SetAccountType sets the "account_type" field. +func (gic *GithubInstallationCreate) SetAccountType(gt githubinstallation.AccountType) *GithubInstallationCreate { + gic.mutation.SetAccountType(gt) + return gic +} + +// SetAccountURL sets the "account_url" field. +func (gic *GithubInstallationCreate) SetAccountURL(s string) *GithubInstallationCreate { + gic.mutation.SetAccountURL(s) + return gic +} + +// SetRepositorySelection sets the "repository_selection" field. +func (gic *GithubInstallationCreate) SetRepositorySelection(gs githubinstallation.RepositorySelection) *GithubInstallationCreate { + gic.mutation.SetRepositorySelection(gs) + return gic +} + +// SetNillableRepositorySelection sets the "repository_selection" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillableRepositorySelection(gs *githubinstallation.RepositorySelection) *GithubInstallationCreate { + if gs != nil { + gic.SetRepositorySelection(*gs) + } + return gic +} + +// SetSuspended sets the "suspended" field. +func (gic *GithubInstallationCreate) SetSuspended(b bool) *GithubInstallationCreate { + gic.mutation.SetSuspended(b) + return gic +} + +// SetNillableSuspended sets the "suspended" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillableSuspended(b *bool) *GithubInstallationCreate { + if b != nil { + gic.SetSuspended(*b) + } + return gic +} + +// SetActive sets the "active" field. +func (gic *GithubInstallationCreate) SetActive(b bool) *GithubInstallationCreate { + gic.mutation.SetActive(b) + return gic +} + +// SetNillableActive sets the "active" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillableActive(b *bool) *GithubInstallationCreate { + if b != nil { + gic.SetActive(*b) + } + return gic +} + +// SetPermissions sets the "permissions" field. +func (gic *GithubInstallationCreate) SetPermissions(mip models.GithubInstallationPermissions) *GithubInstallationCreate { + gic.mutation.SetPermissions(mip) + return gic +} + +// SetNillablePermissions sets the "permissions" field if the given value is not nil. +func (gic *GithubInstallationCreate) SetNillablePermissions(mip *models.GithubInstallationPermissions) *GithubInstallationCreate { + if mip != nil { + gic.SetPermissions(*mip) + } + return gic +} + +// SetEvents sets the "events" field. +func (gic *GithubInstallationCreate) SetEvents(s []string) *GithubInstallationCreate { + gic.mutation.SetEvents(s) + return gic +} + +// SetID sets the "id" field. +func (gic *GithubInstallationCreate) SetID(i int64) *GithubInstallationCreate { + gic.mutation.SetID(i) + return gic +} + +// SetGithubAppsID sets the "github_apps" edge to the GithubApp entity by ID. +func (gic *GithubInstallationCreate) SetGithubAppsID(id int64) *GithubInstallationCreate { + gic.mutation.SetGithubAppsID(id) + return gic +} + +// SetGithubApps sets the "github_apps" edge to the GithubApp entity. +func (gic *GithubInstallationCreate) SetGithubApps(g *GithubApp) *GithubInstallationCreate { + return gic.SetGithubAppsID(g.ID) +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (gic *GithubInstallationCreate) Mutation() *GithubInstallationMutation { + return gic.mutation +} + +// Save creates the GithubInstallation in the database. +func (gic *GithubInstallationCreate) Save(ctx context.Context) (*GithubInstallation, error) { + gic.defaults() + return withHooks(ctx, gic.sqlSave, gic.mutation, gic.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (gic *GithubInstallationCreate) SaveX(ctx context.Context) *GithubInstallation { + v, err := gic.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (gic *GithubInstallationCreate) Exec(ctx context.Context) error { + _, err := gic.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gic *GithubInstallationCreate) ExecX(ctx context.Context) { + if err := gic.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (gic *GithubInstallationCreate) defaults() { + if _, ok := gic.mutation.CreatedAt(); !ok { + v := githubinstallation.DefaultCreatedAt() + gic.mutation.SetCreatedAt(v) + } + if _, ok := gic.mutation.UpdatedAt(); !ok { + v := githubinstallation.DefaultUpdatedAt() + gic.mutation.SetUpdatedAt(v) + } + if _, ok := gic.mutation.RepositorySelection(); !ok { + v := githubinstallation.DefaultRepositorySelection + gic.mutation.SetRepositorySelection(v) + } + if _, ok := gic.mutation.Suspended(); !ok { + v := githubinstallation.DefaultSuspended + gic.mutation.SetSuspended(v) + } + if _, ok := gic.mutation.Active(); !ok { + v := githubinstallation.DefaultActive + gic.mutation.SetActive(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (gic *GithubInstallationCreate) check() error { + if _, ok := gic.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "GithubInstallation.created_at"`)} + } + if _, ok := gic.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "GithubInstallation.updated_at"`)} + } + if _, ok := gic.mutation.GithubAppID(); !ok { + return &ValidationError{Name: "github_app_id", err: errors.New(`ent: missing required field "GithubInstallation.github_app_id"`)} + } + if _, ok := gic.mutation.AccountID(); !ok { + return &ValidationError{Name: "account_id", err: errors.New(`ent: missing required field "GithubInstallation.account_id"`)} + } + if _, ok := gic.mutation.AccountLogin(); !ok { + return &ValidationError{Name: "account_login", err: errors.New(`ent: missing required field "GithubInstallation.account_login"`)} + } + if v, ok := gic.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + if _, ok := gic.mutation.AccountType(); !ok { + return &ValidationError{Name: "account_type", err: errors.New(`ent: missing required field "GithubInstallation.account_type"`)} + } + if v, ok := gic.mutation.AccountType(); ok { + if err := githubinstallation.AccountTypeValidator(v); err != nil { + return &ValidationError{Name: "account_type", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_type": %w`, err)} + } + } + if _, ok := gic.mutation.AccountURL(); !ok { + return &ValidationError{Name: "account_url", err: errors.New(`ent: missing required field "GithubInstallation.account_url"`)} + } + if v, ok := gic.mutation.AccountURL(); ok { + if err := githubinstallation.AccountURLValidator(v); err != nil { + return &ValidationError{Name: "account_url", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_url": %w`, err)} + } + } + if _, ok := gic.mutation.RepositorySelection(); !ok { + return &ValidationError{Name: "repository_selection", err: errors.New(`ent: missing required field "GithubInstallation.repository_selection"`)} + } + if v, ok := gic.mutation.RepositorySelection(); ok { + if err := githubinstallation.RepositorySelectionValidator(v); err != nil { + return &ValidationError{Name: "repository_selection", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.repository_selection": %w`, err)} + } + } + if _, ok := gic.mutation.Suspended(); !ok { + return &ValidationError{Name: "suspended", err: errors.New(`ent: missing required field "GithubInstallation.suspended"`)} + } + if _, ok := gic.mutation.Active(); !ok { + return &ValidationError{Name: "active", err: errors.New(`ent: missing required field "GithubInstallation.active"`)} + } + if v, ok := gic.mutation.ID(); ok { + if err := githubinstallation.IDValidator(v); err != nil { + return &ValidationError{Name: "id", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.id": %w`, err)} + } + } + if len(gic.mutation.GithubAppsIDs()) == 0 { + return &ValidationError{Name: "github_apps", err: errors.New(`ent: missing required edge "GithubInstallation.github_apps"`)} + } + return nil +} + +func (gic *GithubInstallationCreate) sqlSave(ctx context.Context) (*GithubInstallation, error) { + if err := gic.check(); err != nil { + return nil, err + } + _node, _spec := gic.createSpec() + if err := sqlgraph.CreateNode(ctx, gic.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != _node.ID { + id := _spec.ID.Value.(int64) + _node.ID = int64(id) + } + gic.mutation.id = &_node.ID + gic.mutation.done = true + return _node, nil +} + +func (gic *GithubInstallationCreate) createSpec() (*GithubInstallation, *sqlgraph.CreateSpec) { + var ( + _node = &GithubInstallation{config: gic.config} + _spec = sqlgraph.NewCreateSpec(githubinstallation.Table, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + ) + _spec.OnConflict = gic.conflict + if id, ok := gic.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = id + } + if value, ok := gic.mutation.CreatedAt(); ok { + _spec.SetField(githubinstallation.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := gic.mutation.UpdatedAt(); ok { + _spec.SetField(githubinstallation.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := gic.mutation.AccountID(); ok { + _spec.SetField(githubinstallation.FieldAccountID, field.TypeInt64, value) + _node.AccountID = value + } + if value, ok := gic.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + _node.AccountLogin = value + } + if value, ok := gic.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeEnum, value) + _node.AccountType = value + } + if value, ok := gic.mutation.AccountURL(); ok { + _spec.SetField(githubinstallation.FieldAccountURL, field.TypeString, value) + _node.AccountURL = value + } + if value, ok := gic.mutation.RepositorySelection(); ok { + _spec.SetField(githubinstallation.FieldRepositorySelection, field.TypeEnum, value) + _node.RepositorySelection = value + } + if value, ok := gic.mutation.Suspended(); ok { + _spec.SetField(githubinstallation.FieldSuspended, field.TypeBool, value) + _node.Suspended = value + } + if value, ok := gic.mutation.Active(); ok { + _spec.SetField(githubinstallation.FieldActive, field.TypeBool, value) + _node.Active = value + } + if value, ok := gic.mutation.Permissions(); ok { + _spec.SetField(githubinstallation.FieldPermissions, field.TypeJSON, value) + _node.Permissions = value + } + if value, ok := gic.mutation.Events(); ok { + _spec.SetField(githubinstallation.FieldEvents, field.TypeJSON, value) + _node.Events = value + } + if nodes := gic.mutation.GithubAppsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: githubinstallation.GithubAppsTable, + Columns: []string{githubinstallation.GithubAppsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.GithubAppID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } + return _node, _spec +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubInstallation.Create(). +// SetCreatedAt(v). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubInstallationUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (gic *GithubInstallationCreate) OnConflict(opts ...sql.ConflictOption) *GithubInstallationUpsertOne { + gic.conflict = opts + return &GithubInstallationUpsertOne{ + create: gic, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (gic *GithubInstallationCreate) OnConflictColumns(columns ...string) *GithubInstallationUpsertOne { + gic.conflict = append(gic.conflict, sql.ConflictColumns(columns...)) + return &GithubInstallationUpsertOne{ + create: gic, + } +} + +type ( + // GithubInstallationUpsertOne is the builder for "upsert"-ing + // one GithubInstallation node. + GithubInstallationUpsertOne struct { + create *GithubInstallationCreate + } + + // GithubInstallationUpsert is the "OnConflict" setter. + GithubInstallationUpsert struct { + *sql.UpdateSet + } +) + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubInstallationUpsert) SetUpdatedAt(v time.Time) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldUpdatedAt, v) + return u +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateUpdatedAt() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldUpdatedAt) + return u +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubInstallationUpsert) SetGithubAppID(v int64) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldGithubAppID, v) + return u +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateGithubAppID() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldGithubAppID) + return u +} + +// SetAccountID sets the "account_id" field. +func (u *GithubInstallationUpsert) SetAccountID(v int64) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountID, v) + return u +} + +// UpdateAccountID sets the "account_id" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountID() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountID) + return u +} + +// AddAccountID adds v to the "account_id" field. +func (u *GithubInstallationUpsert) AddAccountID(v int64) *GithubInstallationUpsert { + u.Add(githubinstallation.FieldAccountID, v) + return u +} + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsert) SetAccountLogin(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountLogin, v) + return u +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountLogin() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountLogin) + return u +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsert) SetAccountType(v githubinstallation.AccountType) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountType, v) + return u +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountType() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountType) + return u +} + +// SetAccountURL sets the "account_url" field. +func (u *GithubInstallationUpsert) SetAccountURL(v string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldAccountURL, v) + return u +} + +// UpdateAccountURL sets the "account_url" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateAccountURL() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldAccountURL) + return u +} + +// SetRepositorySelection sets the "repository_selection" field. +func (u *GithubInstallationUpsert) SetRepositorySelection(v githubinstallation.RepositorySelection) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldRepositorySelection, v) + return u +} + +// UpdateRepositorySelection sets the "repository_selection" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateRepositorySelection() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldRepositorySelection) + return u +} + +// SetSuspended sets the "suspended" field. +func (u *GithubInstallationUpsert) SetSuspended(v bool) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldSuspended, v) + return u +} + +// UpdateSuspended sets the "suspended" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateSuspended() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldSuspended) + return u +} + +// SetActive sets the "active" field. +func (u *GithubInstallationUpsert) SetActive(v bool) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldActive, v) + return u +} + +// UpdateActive sets the "active" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateActive() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldActive) + return u +} + +// SetPermissions sets the "permissions" field. +func (u *GithubInstallationUpsert) SetPermissions(v models.GithubInstallationPermissions) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldPermissions, v) + return u +} + +// UpdatePermissions sets the "permissions" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdatePermissions() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldPermissions) + return u +} + +// ClearPermissions clears the value of the "permissions" field. +func (u *GithubInstallationUpsert) ClearPermissions() *GithubInstallationUpsert { + u.SetNull(githubinstallation.FieldPermissions) + return u +} + +// SetEvents sets the "events" field. +func (u *GithubInstallationUpsert) SetEvents(v []string) *GithubInstallationUpsert { + u.Set(githubinstallation.FieldEvents, v) + return u +} + +// UpdateEvents sets the "events" field to the value that was provided on create. +func (u *GithubInstallationUpsert) UpdateEvents() *GithubInstallationUpsert { + u.SetExcluded(githubinstallation.FieldEvents) + return u +} + +// ClearEvents clears the value of the "events" field. +func (u *GithubInstallationUpsert) ClearEvents() *GithubInstallationUpsert { + u.SetNull(githubinstallation.FieldEvents) + return u +} + +// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubinstallation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubInstallationUpsertOne) UpdateNewValues() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ID(); exists { + s.SetIgnore(githubinstallation.FieldID) + } + if _, exists := u.create.mutation.CreatedAt(); exists { + s.SetIgnore(githubinstallation.FieldCreatedAt) + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubInstallationUpsertOne) Ignore() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubInstallationUpsertOne) DoNothing() *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubInstallationCreate.OnConflict +// documentation for more info. +func (u *GithubInstallationUpsertOne) Update(set func(*GithubInstallationUpsert)) *GithubInstallationUpsertOne { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubInstallationUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubInstallationUpsertOne) SetUpdatedAt(v time.Time) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateUpdatedAt() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubInstallationUpsertOne) SetGithubAppID(v int64) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetGithubAppID(v) + }) +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateGithubAppID() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateGithubAppID() + }) +} + +// SetAccountID sets the "account_id" field. +func (u *GithubInstallationUpsertOne) SetAccountID(v int64) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountID(v) + }) +} + +// AddAccountID adds v to the "account_id" field. +func (u *GithubInstallationUpsertOne) AddAccountID(v int64) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.AddAccountID(v) + }) +} + +// UpdateAccountID sets the "account_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountID() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountID() + }) +} + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsertOne) SetAccountLogin(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountLogin(v) + }) +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountLogin() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountLogin() + }) +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsertOne) SetAccountType(v githubinstallation.AccountType) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountType(v) + }) +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountType() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountType() + }) +} + +// SetAccountURL sets the "account_url" field. +func (u *GithubInstallationUpsertOne) SetAccountURL(v string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountURL(v) + }) +} + +// UpdateAccountURL sets the "account_url" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateAccountURL() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountURL() + }) +} + +// SetRepositorySelection sets the "repository_selection" field. +func (u *GithubInstallationUpsertOne) SetRepositorySelection(v githubinstallation.RepositorySelection) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetRepositorySelection(v) + }) +} + +// UpdateRepositorySelection sets the "repository_selection" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateRepositorySelection() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateRepositorySelection() + }) +} + +// SetSuspended sets the "suspended" field. +func (u *GithubInstallationUpsertOne) SetSuspended(v bool) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetSuspended(v) + }) +} + +// UpdateSuspended sets the "suspended" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateSuspended() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateSuspended() + }) +} + +// SetActive sets the "active" field. +func (u *GithubInstallationUpsertOne) SetActive(v bool) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetActive(v) + }) +} + +// UpdateActive sets the "active" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateActive() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateActive() + }) +} + +// SetPermissions sets the "permissions" field. +func (u *GithubInstallationUpsertOne) SetPermissions(v models.GithubInstallationPermissions) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetPermissions(v) + }) +} + +// UpdatePermissions sets the "permissions" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdatePermissions() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdatePermissions() + }) +} + +// ClearPermissions clears the value of the "permissions" field. +func (u *GithubInstallationUpsertOne) ClearPermissions() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.ClearPermissions() + }) +} + +// SetEvents sets the "events" field. +func (u *GithubInstallationUpsertOne) SetEvents(v []string) *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetEvents(v) + }) +} + +// UpdateEvents sets the "events" field to the value that was provided on create. +func (u *GithubInstallationUpsertOne) UpdateEvents() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateEvents() + }) +} + +// ClearEvents clears the value of the "events" field. +func (u *GithubInstallationUpsertOne) ClearEvents() *GithubInstallationUpsertOne { + return u.Update(func(s *GithubInstallationUpsert) { + s.ClearEvents() + }) +} + +// Exec executes the query. +func (u *GithubInstallationUpsertOne) Exec(ctx context.Context) error { + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubInstallationCreate.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubInstallationUpsertOne) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} + +// Exec executes the UPSERT query and returns the inserted/updated ID. +func (u *GithubInstallationUpsertOne) ID(ctx context.Context) (id int64, err error) { + node, err := u.create.Save(ctx) + if err != nil { + return id, err + } + return node.ID, nil +} + +// IDX is like ID, but panics if an error occurs. +func (u *GithubInstallationUpsertOne) IDX(ctx context.Context) int64 { + id, err := u.ID(ctx) + if err != nil { + panic(err) + } + return id +} + +// GithubInstallationCreateBulk is the builder for creating many GithubInstallation entities in bulk. +type GithubInstallationCreateBulk struct { + config + err error + builders []*GithubInstallationCreate + conflict []sql.ConflictOption +} + +// Save creates the GithubInstallation entities in the database. +func (gicb *GithubInstallationCreateBulk) Save(ctx context.Context) ([]*GithubInstallation, error) { + if gicb.err != nil { + return nil, gicb.err + } + specs := make([]*sqlgraph.CreateSpec, len(gicb.builders)) + nodes := make([]*GithubInstallation, len(gicb.builders)) + mutators := make([]Mutator, len(gicb.builders)) + for i := range gicb.builders { + func(i int, root context.Context) { + builder := gicb.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*GithubInstallationMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, gicb.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + spec.OnConflict = gicb.conflict + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, gicb.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + if specs[i].ID.Value != nil && nodes[i].ID == 0 { + id := specs[i].ID.Value.(int64) + nodes[i].ID = int64(id) + } + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, gicb.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (gicb *GithubInstallationCreateBulk) SaveX(ctx context.Context) []*GithubInstallation { + v, err := gicb.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (gicb *GithubInstallationCreateBulk) Exec(ctx context.Context) error { + _, err := gicb.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (gicb *GithubInstallationCreateBulk) ExecX(ctx context.Context) { + if err := gicb.Exec(ctx); err != nil { + panic(err) + } +} + +// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause +// of the `INSERT` statement. For example: +// +// client.GithubInstallation.CreateBulk(builders...). +// OnConflict( +// // Update the row with the new values +// // the was proposed for insertion. +// sql.ResolveWithNewValues(), +// ). +// // Override some of the fields with custom +// // update values. +// Update(func(u *ent.GithubInstallationUpsert) { +// SetCreatedAt(v+v). +// }). +// Exec(ctx) +func (gicb *GithubInstallationCreateBulk) OnConflict(opts ...sql.ConflictOption) *GithubInstallationUpsertBulk { + gicb.conflict = opts + return &GithubInstallationUpsertBulk{ + create: gicb, + } +} + +// OnConflictColumns calls `OnConflict` and configures the columns +// as conflict target. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ConflictColumns(columns...)). +// Exec(ctx) +func (gicb *GithubInstallationCreateBulk) OnConflictColumns(columns ...string) *GithubInstallationUpsertBulk { + gicb.conflict = append(gicb.conflict, sql.ConflictColumns(columns...)) + return &GithubInstallationUpsertBulk{ + create: gicb, + } +} + +// GithubInstallationUpsertBulk is the builder for "upsert"-ing +// a bulk of GithubInstallation nodes. +type GithubInstallationUpsertBulk struct { + create *GithubInstallationCreateBulk +} + +// UpdateNewValues updates the mutable fields using the new values that +// were set on create. Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict( +// sql.ResolveWithNewValues(), +// sql.ResolveWith(func(u *sql.UpdateSet) { +// u.SetIgnore(githubinstallation.FieldID) +// }), +// ). +// Exec(ctx) +func (u *GithubInstallationUpsertBulk) UpdateNewValues() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ID(); exists { + s.SetIgnore(githubinstallation.FieldID) + } + if _, exists := b.mutation.CreatedAt(); exists { + s.SetIgnore(githubinstallation.FieldCreatedAt) + } + } + })) + return u +} + +// Ignore sets each column to itself in case of conflict. +// Using this option is equivalent to using: +// +// client.GithubInstallation.Create(). +// OnConflict(sql.ResolveWithIgnore()). +// Exec(ctx) +func (u *GithubInstallationUpsertBulk) Ignore() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore()) + return u +} + +// DoNothing configures the conflict_action to `DO NOTHING`. +// Supported only by SQLite and PostgreSQL. +func (u *GithubInstallationUpsertBulk) DoNothing() *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.DoNothing()) + return u +} + +// Update allows overriding fields `UPDATE` values. See the GithubInstallationCreateBulk.OnConflict +// documentation for more info. +func (u *GithubInstallationUpsertBulk) Update(set func(*GithubInstallationUpsert)) *GithubInstallationUpsertBulk { + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) { + set(&GithubInstallationUpsert{UpdateSet: update}) + })) + return u +} + +// SetUpdatedAt sets the "updated_at" field. +func (u *GithubInstallationUpsertBulk) SetUpdatedAt(v time.Time) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetUpdatedAt(v) + }) +} + +// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateUpdatedAt() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateUpdatedAt() + }) +} + +// SetGithubAppID sets the "github_app_id" field. +func (u *GithubInstallationUpsertBulk) SetGithubAppID(v int64) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetGithubAppID(v) + }) +} + +// UpdateGithubAppID sets the "github_app_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateGithubAppID() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateGithubAppID() + }) +} + +// SetAccountID sets the "account_id" field. +func (u *GithubInstallationUpsertBulk) SetAccountID(v int64) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountID(v) + }) +} + +// AddAccountID adds v to the "account_id" field. +func (u *GithubInstallationUpsertBulk) AddAccountID(v int64) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.AddAccountID(v) + }) +} + +// UpdateAccountID sets the "account_id" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountID() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountID() + }) +} + +// SetAccountLogin sets the "account_login" field. +func (u *GithubInstallationUpsertBulk) SetAccountLogin(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountLogin(v) + }) +} + +// UpdateAccountLogin sets the "account_login" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountLogin() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountLogin() + }) +} + +// SetAccountType sets the "account_type" field. +func (u *GithubInstallationUpsertBulk) SetAccountType(v githubinstallation.AccountType) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountType(v) + }) +} + +// UpdateAccountType sets the "account_type" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountType() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountType() + }) +} + +// SetAccountURL sets the "account_url" field. +func (u *GithubInstallationUpsertBulk) SetAccountURL(v string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetAccountURL(v) + }) +} + +// UpdateAccountURL sets the "account_url" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateAccountURL() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateAccountURL() + }) +} + +// SetRepositorySelection sets the "repository_selection" field. +func (u *GithubInstallationUpsertBulk) SetRepositorySelection(v githubinstallation.RepositorySelection) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetRepositorySelection(v) + }) +} + +// UpdateRepositorySelection sets the "repository_selection" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateRepositorySelection() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateRepositorySelection() + }) +} + +// SetSuspended sets the "suspended" field. +func (u *GithubInstallationUpsertBulk) SetSuspended(v bool) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetSuspended(v) + }) +} + +// UpdateSuspended sets the "suspended" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateSuspended() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateSuspended() + }) +} + +// SetActive sets the "active" field. +func (u *GithubInstallationUpsertBulk) SetActive(v bool) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetActive(v) + }) +} + +// UpdateActive sets the "active" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateActive() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateActive() + }) +} + +// SetPermissions sets the "permissions" field. +func (u *GithubInstallationUpsertBulk) SetPermissions(v models.GithubInstallationPermissions) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetPermissions(v) + }) +} + +// UpdatePermissions sets the "permissions" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdatePermissions() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdatePermissions() + }) +} + +// ClearPermissions clears the value of the "permissions" field. +func (u *GithubInstallationUpsertBulk) ClearPermissions() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.ClearPermissions() + }) +} + +// SetEvents sets the "events" field. +func (u *GithubInstallationUpsertBulk) SetEvents(v []string) *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.SetEvents(v) + }) +} + +// UpdateEvents sets the "events" field to the value that was provided on create. +func (u *GithubInstallationUpsertBulk) UpdateEvents() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.UpdateEvents() + }) +} + +// ClearEvents clears the value of the "events" field. +func (u *GithubInstallationUpsertBulk) ClearEvents() *GithubInstallationUpsertBulk { + return u.Update(func(s *GithubInstallationUpsert) { + s.ClearEvents() + }) +} + +// Exec executes the query. +func (u *GithubInstallationUpsertBulk) Exec(ctx context.Context) error { + if u.create.err != nil { + return u.create.err + } + for i, b := range u.create.builders { + if len(b.conflict) != 0 { + return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the GithubInstallationCreateBulk instead", i) + } + } + if len(u.create.conflict) == 0 { + return errors.New("ent: missing options for GithubInstallationCreateBulk.OnConflict") + } + return u.create.Exec(ctx) +} + +// ExecX is like Exec, but panics if an error occurs. +func (u *GithubInstallationUpsertBulk) ExecX(ctx context.Context) { + if err := u.create.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/githubinstallation_delete.go b/ent/githubinstallation_delete.go new file mode 100644 index 00000000..0441cd68 --- /dev/null +++ b/ent/githubinstallation_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// GithubInstallationDelete is the builder for deleting a GithubInstallation entity. +type GithubInstallationDelete struct { + config + hooks []Hook + mutation *GithubInstallationMutation +} + +// Where appends a list predicates to the GithubInstallationDelete builder. +func (gid *GithubInstallationDelete) Where(ps ...predicate.GithubInstallation) *GithubInstallationDelete { + gid.mutation.Where(ps...) + return gid +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (gid *GithubInstallationDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, gid.sqlExec, gid.mutation, gid.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (gid *GithubInstallationDelete) ExecX(ctx context.Context) int { + n, err := gid.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (gid *GithubInstallationDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(githubinstallation.Table, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + if ps := gid.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, gid.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + gid.mutation.done = true + return affected, err +} + +// GithubInstallationDeleteOne is the builder for deleting a single GithubInstallation entity. +type GithubInstallationDeleteOne struct { + gid *GithubInstallationDelete +} + +// Where appends a list predicates to the GithubInstallationDelete builder. +func (gido *GithubInstallationDeleteOne) Where(ps ...predicate.GithubInstallation) *GithubInstallationDeleteOne { + gido.gid.mutation.Where(ps...) + return gido +} + +// Exec executes the deletion query. +func (gido *GithubInstallationDeleteOne) Exec(ctx context.Context) error { + n, err := gido.gid.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{githubinstallation.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (gido *GithubInstallationDeleteOne) ExecX(ctx context.Context) { + if err := gido.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/githubinstallation_query.go b/ent/githubinstallation_query.go new file mode 100644 index 00000000..5f45e52f --- /dev/null +++ b/ent/githubinstallation_query.go @@ -0,0 +1,629 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/ent/predicate" +) + +// GithubInstallationQuery is the builder for querying GithubInstallation entities. +type GithubInstallationQuery struct { + config + ctx *QueryContext + order []githubinstallation.OrderOption + inters []Interceptor + predicates []predicate.GithubInstallation + withGithubApps *GithubAppQuery + modifiers []func(*sql.Selector) + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the GithubInstallationQuery builder. +func (giq *GithubInstallationQuery) Where(ps ...predicate.GithubInstallation) *GithubInstallationQuery { + giq.predicates = append(giq.predicates, ps...) + return giq +} + +// Limit the number of records to be returned by this query. +func (giq *GithubInstallationQuery) Limit(limit int) *GithubInstallationQuery { + giq.ctx.Limit = &limit + return giq +} + +// Offset to start from. +func (giq *GithubInstallationQuery) Offset(offset int) *GithubInstallationQuery { + giq.ctx.Offset = &offset + return giq +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (giq *GithubInstallationQuery) Unique(unique bool) *GithubInstallationQuery { + giq.ctx.Unique = &unique + return giq +} + +// Order specifies how the records should be ordered. +func (giq *GithubInstallationQuery) Order(o ...githubinstallation.OrderOption) *GithubInstallationQuery { + giq.order = append(giq.order, o...) + return giq +} + +// QueryGithubApps chains the current query on the "github_apps" edge. +func (giq *GithubInstallationQuery) QueryGithubApps() *GithubAppQuery { + query := (&GithubAppClient{config: giq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := giq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := giq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(githubinstallation.Table, githubinstallation.FieldID, selector), + sqlgraph.To(githubapp.Table, githubapp.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, githubinstallation.GithubAppsTable, githubinstallation.GithubAppsColumn), + ) + fromU = sqlgraph.SetNeighbors(giq.driver.Dialect(), step) + return fromU, nil + } + return query +} + +// First returns the first GithubInstallation entity from the query. +// Returns a *NotFoundError when no GithubInstallation was found. +func (giq *GithubInstallationQuery) First(ctx context.Context) (*GithubInstallation, error) { + nodes, err := giq.Limit(1).All(setContextOp(ctx, giq.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{githubinstallation.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (giq *GithubInstallationQuery) FirstX(ctx context.Context) *GithubInstallation { + node, err := giq.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first GithubInstallation ID from the query. +// Returns a *NotFoundError when no GithubInstallation ID was found. +func (giq *GithubInstallationQuery) FirstID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = giq.Limit(1).IDs(setContextOp(ctx, giq.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{githubinstallation.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (giq *GithubInstallationQuery) FirstIDX(ctx context.Context) int64 { + id, err := giq.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single GithubInstallation entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one GithubInstallation entity is found. +// Returns a *NotFoundError when no GithubInstallation entities are found. +func (giq *GithubInstallationQuery) Only(ctx context.Context) (*GithubInstallation, error) { + nodes, err := giq.Limit(2).All(setContextOp(ctx, giq.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{githubinstallation.Label} + default: + return nil, &NotSingularError{githubinstallation.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (giq *GithubInstallationQuery) OnlyX(ctx context.Context) *GithubInstallation { + node, err := giq.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only GithubInstallation ID in the query. +// Returns a *NotSingularError when more than one GithubInstallation ID is found. +// Returns a *NotFoundError when no entities are found. +func (giq *GithubInstallationQuery) OnlyID(ctx context.Context) (id int64, err error) { + var ids []int64 + if ids, err = giq.Limit(2).IDs(setContextOp(ctx, giq.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{githubinstallation.Label} + default: + err = &NotSingularError{githubinstallation.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (giq *GithubInstallationQuery) OnlyIDX(ctx context.Context) int64 { + id, err := giq.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of GithubInstallations. +func (giq *GithubInstallationQuery) All(ctx context.Context) ([]*GithubInstallation, error) { + ctx = setContextOp(ctx, giq.ctx, ent.OpQueryAll) + if err := giq.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*GithubInstallation, *GithubInstallationQuery]() + return withInterceptors[[]*GithubInstallation](ctx, giq, qr, giq.inters) +} + +// AllX is like All, but panics if an error occurs. +func (giq *GithubInstallationQuery) AllX(ctx context.Context) []*GithubInstallation { + nodes, err := giq.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of GithubInstallation IDs. +func (giq *GithubInstallationQuery) IDs(ctx context.Context) (ids []int64, err error) { + if giq.ctx.Unique == nil && giq.path != nil { + giq.Unique(true) + } + ctx = setContextOp(ctx, giq.ctx, ent.OpQueryIDs) + if err = giq.Select(githubinstallation.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (giq *GithubInstallationQuery) IDsX(ctx context.Context) []int64 { + ids, err := giq.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (giq *GithubInstallationQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, giq.ctx, ent.OpQueryCount) + if err := giq.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, giq, querierCount[*GithubInstallationQuery](), giq.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (giq *GithubInstallationQuery) CountX(ctx context.Context) int { + count, err := giq.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (giq *GithubInstallationQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, giq.ctx, ent.OpQueryExist) + switch _, err := giq.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (giq *GithubInstallationQuery) ExistX(ctx context.Context) bool { + exist, err := giq.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the GithubInstallationQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (giq *GithubInstallationQuery) Clone() *GithubInstallationQuery { + if giq == nil { + return nil + } + return &GithubInstallationQuery{ + config: giq.config, + ctx: giq.ctx.Clone(), + order: append([]githubinstallation.OrderOption{}, giq.order...), + inters: append([]Interceptor{}, giq.inters...), + predicates: append([]predicate.GithubInstallation{}, giq.predicates...), + withGithubApps: giq.withGithubApps.Clone(), + // clone intermediate query. + sql: giq.sql.Clone(), + path: giq.path, + modifiers: append([]func(*sql.Selector){}, giq.modifiers...), + } +} + +// WithGithubApps tells the query-builder to eager-load the nodes that are connected to +// the "github_apps" edge. The optional arguments are used to configure the query builder of the edge. +func (giq *GithubInstallationQuery) WithGithubApps(opts ...func(*GithubAppQuery)) *GithubInstallationQuery { + query := (&GithubAppClient{config: giq.config}).Query() + for _, opt := range opts { + opt(query) + } + giq.withGithubApps = query + return giq +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.GithubInstallation.Query(). +// GroupBy(githubinstallation.FieldCreatedAt). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (giq *GithubInstallationQuery) GroupBy(field string, fields ...string) *GithubInstallationGroupBy { + giq.ctx.Fields = append([]string{field}, fields...) + grbuild := &GithubInstallationGroupBy{build: giq} + grbuild.flds = &giq.ctx.Fields + grbuild.label = githubinstallation.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// CreatedAt time.Time `json:"created_at,omitempty"` +// } +// +// client.GithubInstallation.Query(). +// Select(githubinstallation.FieldCreatedAt). +// Scan(ctx, &v) +func (giq *GithubInstallationQuery) Select(fields ...string) *GithubInstallationSelect { + giq.ctx.Fields = append(giq.ctx.Fields, fields...) + sbuild := &GithubInstallationSelect{GithubInstallationQuery: giq} + sbuild.label = githubinstallation.Label + sbuild.flds, sbuild.scan = &giq.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a GithubInstallationSelect configured with the given aggregations. +func (giq *GithubInstallationQuery) Aggregate(fns ...AggregateFunc) *GithubInstallationSelect { + return giq.Select().Aggregate(fns...) +} + +func (giq *GithubInstallationQuery) prepareQuery(ctx context.Context) error { + for _, inter := range giq.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, giq); err != nil { + return err + } + } + } + for _, f := range giq.ctx.Fields { + if !githubinstallation.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if giq.path != nil { + prev, err := giq.path(ctx) + if err != nil { + return err + } + giq.sql = prev + } + return nil +} + +func (giq *GithubInstallationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*GithubInstallation, error) { + var ( + nodes = []*GithubInstallation{} + _spec = giq.querySpec() + loadedTypes = [1]bool{ + giq.withGithubApps != nil, + } + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*GithubInstallation).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &GithubInstallation{config: giq.config} + nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes + return node.assignValues(columns, values) + } + if len(giq.modifiers) > 0 { + _spec.Modifiers = giq.modifiers + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, giq.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + if query := giq.withGithubApps; query != nil { + if err := giq.loadGithubApps(ctx, query, nodes, nil, + func(n *GithubInstallation, e *GithubApp) { n.Edges.GithubApps = e }); err != nil { + return nil, err + } + } + return nodes, nil +} + +func (giq *GithubInstallationQuery) loadGithubApps(ctx context.Context, query *GithubAppQuery, nodes []*GithubInstallation, init func(*GithubInstallation), assign func(*GithubInstallation, *GithubApp)) error { + ids := make([]int64, 0, len(nodes)) + nodeids := make(map[int64][]*GithubInstallation) + for i := range nodes { + fk := nodes[i].GithubAppID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(githubapp.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "github_app_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} + +func (giq *GithubInstallationQuery) sqlCount(ctx context.Context) (int, error) { + _spec := giq.querySpec() + if len(giq.modifiers) > 0 { + _spec.Modifiers = giq.modifiers + } + _spec.Node.Columns = giq.ctx.Fields + if len(giq.ctx.Fields) > 0 { + _spec.Unique = giq.ctx.Unique != nil && *giq.ctx.Unique + } + return sqlgraph.CountNodes(ctx, giq.driver, _spec) +} + +func (giq *GithubInstallationQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + _spec.From = giq.sql + if unique := giq.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if giq.path != nil { + _spec.Unique = true + } + if fields := giq.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubinstallation.FieldID) + for i := range fields { + if fields[i] != githubinstallation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + if giq.withGithubApps != nil { + _spec.Node.AddColumnOnce(githubinstallation.FieldGithubAppID) + } + } + if ps := giq.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := giq.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := giq.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := giq.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (giq *GithubInstallationQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(giq.driver.Dialect()) + t1 := builder.Table(githubinstallation.Table) + columns := giq.ctx.Fields + if len(columns) == 0 { + columns = githubinstallation.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if giq.sql != nil { + selector = giq.sql + selector.Select(selector.Columns(columns...)...) + } + if giq.ctx.Unique != nil && *giq.ctx.Unique { + selector.Distinct() + } + for _, m := range giq.modifiers { + m(selector) + } + for _, p := range giq.predicates { + p(selector) + } + for _, p := range giq.order { + p(selector) + } + if offset := giq.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := giq.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (giq *GithubInstallationQuery) Modify(modifiers ...func(s *sql.Selector)) *GithubInstallationSelect { + giq.modifiers = append(giq.modifiers, modifiers...) + return giq.Select() +} + +// GithubInstallationGroupBy is the group-by builder for GithubInstallation entities. +type GithubInstallationGroupBy struct { + selector + build *GithubInstallationQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (gigb *GithubInstallationGroupBy) Aggregate(fns ...AggregateFunc) *GithubInstallationGroupBy { + gigb.fns = append(gigb.fns, fns...) + return gigb +} + +// Scan applies the selector query and scans the result into the given value. +func (gigb *GithubInstallationGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, gigb.build.ctx, ent.OpQueryGroupBy) + if err := gigb.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubInstallationQuery, *GithubInstallationGroupBy](ctx, gigb.build, gigb, gigb.build.inters, v) +} + +func (gigb *GithubInstallationGroupBy) sqlScan(ctx context.Context, root *GithubInstallationQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(gigb.fns)) + for _, fn := range gigb.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*gigb.flds)+len(gigb.fns)) + for _, f := range *gigb.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*gigb.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := gigb.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// GithubInstallationSelect is the builder for selecting fields of GithubInstallation entities. +type GithubInstallationSelect struct { + *GithubInstallationQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (gis *GithubInstallationSelect) Aggregate(fns ...AggregateFunc) *GithubInstallationSelect { + gis.fns = append(gis.fns, fns...) + return gis +} + +// Scan applies the selector query and scans the result into the given value. +func (gis *GithubInstallationSelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, gis.ctx, ent.OpQuerySelect) + if err := gis.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*GithubInstallationQuery, *GithubInstallationSelect](ctx, gis.GithubInstallationQuery, gis, gis.inters, v) +} + +func (gis *GithubInstallationSelect) sqlScan(ctx context.Context, root *GithubInstallationQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(gis.fns)) + for _, fn := range gis.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*gis.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := gis.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// Modify adds a query modifier for attaching custom logic to queries. +func (gis *GithubInstallationSelect) Modify(modifiers ...func(s *sql.Selector)) *GithubInstallationSelect { + gis.modifiers = append(gis.modifiers, modifiers...) + return gis +} diff --git a/ent/githubinstallation_update.go b/ent/githubinstallation_update.go new file mode 100644 index 00000000..6cf88ce8 --- /dev/null +++ b/ent/githubinstallation_update.go @@ -0,0 +1,781 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" + "entgo.io/ent/schema/field" + "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/ent/predicate" + "github.com/unbindapp/unbind-api/internal/models" +) + +// GithubInstallationUpdate is the builder for updating GithubInstallation entities. +type GithubInstallationUpdate struct { + config + hooks []Hook + mutation *GithubInstallationMutation + modifiers []func(*sql.UpdateBuilder) +} + +// Where appends a list predicates to the GithubInstallationUpdate builder. +func (giu *GithubInstallationUpdate) Where(ps ...predicate.GithubInstallation) *GithubInstallationUpdate { + giu.mutation.Where(ps...) + return giu +} + +// SetUpdatedAt sets the "updated_at" field. +func (giu *GithubInstallationUpdate) SetUpdatedAt(t time.Time) *GithubInstallationUpdate { + giu.mutation.SetUpdatedAt(t) + return giu +} + +// SetGithubAppID sets the "github_app_id" field. +func (giu *GithubInstallationUpdate) SetGithubAppID(i int64) *GithubInstallationUpdate { + giu.mutation.SetGithubAppID(i) + return giu +} + +// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableGithubAppID(i *int64) *GithubInstallationUpdate { + if i != nil { + giu.SetGithubAppID(*i) + } + return giu +} + +// SetAccountID sets the "account_id" field. +func (giu *GithubInstallationUpdate) SetAccountID(i int64) *GithubInstallationUpdate { + giu.mutation.ResetAccountID() + giu.mutation.SetAccountID(i) + return giu +} + +// SetNillableAccountID sets the "account_id" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableAccountID(i *int64) *GithubInstallationUpdate { + if i != nil { + giu.SetAccountID(*i) + } + return giu +} + +// AddAccountID adds i to the "account_id" field. +func (giu *GithubInstallationUpdate) AddAccountID(i int64) *GithubInstallationUpdate { + giu.mutation.AddAccountID(i) + return giu +} + +// SetAccountLogin sets the "account_login" field. +func (giu *GithubInstallationUpdate) SetAccountLogin(s string) *GithubInstallationUpdate { + giu.mutation.SetAccountLogin(s) + return giu +} + +// SetNillableAccountLogin sets the "account_login" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableAccountLogin(s *string) *GithubInstallationUpdate { + if s != nil { + giu.SetAccountLogin(*s) + } + return giu +} + +// SetAccountType sets the "account_type" field. +func (giu *GithubInstallationUpdate) SetAccountType(gt githubinstallation.AccountType) *GithubInstallationUpdate { + giu.mutation.SetAccountType(gt) + return giu +} + +// SetNillableAccountType sets the "account_type" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableAccountType(gt *githubinstallation.AccountType) *GithubInstallationUpdate { + if gt != nil { + giu.SetAccountType(*gt) + } + return giu +} + +// SetAccountURL sets the "account_url" field. +func (giu *GithubInstallationUpdate) SetAccountURL(s string) *GithubInstallationUpdate { + giu.mutation.SetAccountURL(s) + return giu +} + +// SetNillableAccountURL sets the "account_url" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableAccountURL(s *string) *GithubInstallationUpdate { + if s != nil { + giu.SetAccountURL(*s) + } + return giu +} + +// SetRepositorySelection sets the "repository_selection" field. +func (giu *GithubInstallationUpdate) SetRepositorySelection(gs githubinstallation.RepositorySelection) *GithubInstallationUpdate { + giu.mutation.SetRepositorySelection(gs) + return giu +} + +// SetNillableRepositorySelection sets the "repository_selection" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableRepositorySelection(gs *githubinstallation.RepositorySelection) *GithubInstallationUpdate { + if gs != nil { + giu.SetRepositorySelection(*gs) + } + return giu +} + +// SetSuspended sets the "suspended" field. +func (giu *GithubInstallationUpdate) SetSuspended(b bool) *GithubInstallationUpdate { + giu.mutation.SetSuspended(b) + return giu +} + +// SetNillableSuspended sets the "suspended" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableSuspended(b *bool) *GithubInstallationUpdate { + if b != nil { + giu.SetSuspended(*b) + } + return giu +} + +// SetActive sets the "active" field. +func (giu *GithubInstallationUpdate) SetActive(b bool) *GithubInstallationUpdate { + giu.mutation.SetActive(b) + return giu +} + +// SetNillableActive sets the "active" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillableActive(b *bool) *GithubInstallationUpdate { + if b != nil { + giu.SetActive(*b) + } + return giu +} + +// SetPermissions sets the "permissions" field. +func (giu *GithubInstallationUpdate) SetPermissions(mip models.GithubInstallationPermissions) *GithubInstallationUpdate { + giu.mutation.SetPermissions(mip) + return giu +} + +// SetNillablePermissions sets the "permissions" field if the given value is not nil. +func (giu *GithubInstallationUpdate) SetNillablePermissions(mip *models.GithubInstallationPermissions) *GithubInstallationUpdate { + if mip != nil { + giu.SetPermissions(*mip) + } + return giu +} + +// ClearPermissions clears the value of the "permissions" field. +func (giu *GithubInstallationUpdate) ClearPermissions() *GithubInstallationUpdate { + giu.mutation.ClearPermissions() + return giu +} + +// SetEvents sets the "events" field. +func (giu *GithubInstallationUpdate) SetEvents(s []string) *GithubInstallationUpdate { + giu.mutation.SetEvents(s) + return giu +} + +// AppendEvents appends s to the "events" field. +func (giu *GithubInstallationUpdate) AppendEvents(s []string) *GithubInstallationUpdate { + giu.mutation.AppendEvents(s) + return giu +} + +// ClearEvents clears the value of the "events" field. +func (giu *GithubInstallationUpdate) ClearEvents() *GithubInstallationUpdate { + giu.mutation.ClearEvents() + return giu +} + +// SetGithubAppsID sets the "github_apps" edge to the GithubApp entity by ID. +func (giu *GithubInstallationUpdate) SetGithubAppsID(id int64) *GithubInstallationUpdate { + giu.mutation.SetGithubAppsID(id) + return giu +} + +// SetGithubApps sets the "github_apps" edge to the GithubApp entity. +func (giu *GithubInstallationUpdate) SetGithubApps(g *GithubApp) *GithubInstallationUpdate { + return giu.SetGithubAppsID(g.ID) +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (giu *GithubInstallationUpdate) Mutation() *GithubInstallationMutation { + return giu.mutation +} + +// ClearGithubApps clears the "github_apps" edge to the GithubApp entity. +func (giu *GithubInstallationUpdate) ClearGithubApps() *GithubInstallationUpdate { + giu.mutation.ClearGithubApps() + return giu +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (giu *GithubInstallationUpdate) Save(ctx context.Context) (int, error) { + giu.defaults() + return withHooks(ctx, giu.sqlSave, giu.mutation, giu.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (giu *GithubInstallationUpdate) SaveX(ctx context.Context) int { + affected, err := giu.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (giu *GithubInstallationUpdate) Exec(ctx context.Context) error { + _, err := giu.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (giu *GithubInstallationUpdate) ExecX(ctx context.Context) { + if err := giu.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (giu *GithubInstallationUpdate) defaults() { + if _, ok := giu.mutation.UpdatedAt(); !ok { + v := githubinstallation.UpdateDefaultUpdatedAt() + giu.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (giu *GithubInstallationUpdate) check() error { + if v, ok := giu.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + if v, ok := giu.mutation.AccountType(); ok { + if err := githubinstallation.AccountTypeValidator(v); err != nil { + return &ValidationError{Name: "account_type", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_type": %w`, err)} + } + } + if v, ok := giu.mutation.AccountURL(); ok { + if err := githubinstallation.AccountURLValidator(v); err != nil { + return &ValidationError{Name: "account_url", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_url": %w`, err)} + } + } + if v, ok := giu.mutation.RepositorySelection(); ok { + if err := githubinstallation.RepositorySelectionValidator(v); err != nil { + return &ValidationError{Name: "repository_selection", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.repository_selection": %w`, err)} + } + } + if giu.mutation.GithubAppsCleared() && len(giu.mutation.GithubAppsIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "GithubInstallation.github_apps"`) + } + return nil +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (giu *GithubInstallationUpdate) Modify(modifiers ...func(u *sql.UpdateBuilder)) *GithubInstallationUpdate { + giu.modifiers = append(giu.modifiers, modifiers...) + return giu +} + +func (giu *GithubInstallationUpdate) sqlSave(ctx context.Context) (n int, err error) { + if err := giu.check(); err != nil { + return n, err + } + _spec := sqlgraph.NewUpdateSpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + if ps := giu.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := giu.mutation.UpdatedAt(); ok { + _spec.SetField(githubinstallation.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := giu.mutation.AccountID(); ok { + _spec.SetField(githubinstallation.FieldAccountID, field.TypeInt64, value) + } + if value, ok := giu.mutation.AddedAccountID(); ok { + _spec.AddField(githubinstallation.FieldAccountID, field.TypeInt64, value) + } + if value, ok := giu.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + } + if value, ok := giu.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeEnum, value) + } + if value, ok := giu.mutation.AccountURL(); ok { + _spec.SetField(githubinstallation.FieldAccountURL, field.TypeString, value) + } + if value, ok := giu.mutation.RepositorySelection(); ok { + _spec.SetField(githubinstallation.FieldRepositorySelection, field.TypeEnum, value) + } + if value, ok := giu.mutation.Suspended(); ok { + _spec.SetField(githubinstallation.FieldSuspended, field.TypeBool, value) + } + if value, ok := giu.mutation.Active(); ok { + _spec.SetField(githubinstallation.FieldActive, field.TypeBool, value) + } + if value, ok := giu.mutation.Permissions(); ok { + _spec.SetField(githubinstallation.FieldPermissions, field.TypeJSON, value) + } + if giu.mutation.PermissionsCleared() { + _spec.ClearField(githubinstallation.FieldPermissions, field.TypeJSON) + } + if value, ok := giu.mutation.Events(); ok { + _spec.SetField(githubinstallation.FieldEvents, field.TypeJSON, value) + } + if value, ok := giu.mutation.AppendedEvents(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, githubinstallation.FieldEvents, value) + }) + } + if giu.mutation.EventsCleared() { + _spec.ClearField(githubinstallation.FieldEvents, field.TypeJSON) + } + if giu.mutation.GithubAppsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: githubinstallation.GithubAppsTable, + Columns: []string{githubinstallation.GithubAppsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := giu.mutation.GithubAppsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: githubinstallation.GithubAppsTable, + Columns: []string{githubinstallation.GithubAppsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + _spec.AddModifiers(giu.modifiers...) + if n, err = sqlgraph.UpdateNodes(ctx, giu.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubinstallation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + giu.mutation.done = true + return n, nil +} + +// GithubInstallationUpdateOne is the builder for updating a single GithubInstallation entity. +type GithubInstallationUpdateOne struct { + config + fields []string + hooks []Hook + mutation *GithubInstallationMutation + modifiers []func(*sql.UpdateBuilder) +} + +// SetUpdatedAt sets the "updated_at" field. +func (giuo *GithubInstallationUpdateOne) SetUpdatedAt(t time.Time) *GithubInstallationUpdateOne { + giuo.mutation.SetUpdatedAt(t) + return giuo +} + +// SetGithubAppID sets the "github_app_id" field. +func (giuo *GithubInstallationUpdateOne) SetGithubAppID(i int64) *GithubInstallationUpdateOne { + giuo.mutation.SetGithubAppID(i) + return giuo +} + +// SetNillableGithubAppID sets the "github_app_id" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableGithubAppID(i *int64) *GithubInstallationUpdateOne { + if i != nil { + giuo.SetGithubAppID(*i) + } + return giuo +} + +// SetAccountID sets the "account_id" field. +func (giuo *GithubInstallationUpdateOne) SetAccountID(i int64) *GithubInstallationUpdateOne { + giuo.mutation.ResetAccountID() + giuo.mutation.SetAccountID(i) + return giuo +} + +// SetNillableAccountID sets the "account_id" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableAccountID(i *int64) *GithubInstallationUpdateOne { + if i != nil { + giuo.SetAccountID(*i) + } + return giuo +} + +// AddAccountID adds i to the "account_id" field. +func (giuo *GithubInstallationUpdateOne) AddAccountID(i int64) *GithubInstallationUpdateOne { + giuo.mutation.AddAccountID(i) + return giuo +} + +// SetAccountLogin sets the "account_login" field. +func (giuo *GithubInstallationUpdateOne) SetAccountLogin(s string) *GithubInstallationUpdateOne { + giuo.mutation.SetAccountLogin(s) + return giuo +} + +// SetNillableAccountLogin sets the "account_login" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableAccountLogin(s *string) *GithubInstallationUpdateOne { + if s != nil { + giuo.SetAccountLogin(*s) + } + return giuo +} + +// SetAccountType sets the "account_type" field. +func (giuo *GithubInstallationUpdateOne) SetAccountType(gt githubinstallation.AccountType) *GithubInstallationUpdateOne { + giuo.mutation.SetAccountType(gt) + return giuo +} + +// SetNillableAccountType sets the "account_type" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableAccountType(gt *githubinstallation.AccountType) *GithubInstallationUpdateOne { + if gt != nil { + giuo.SetAccountType(*gt) + } + return giuo +} + +// SetAccountURL sets the "account_url" field. +func (giuo *GithubInstallationUpdateOne) SetAccountURL(s string) *GithubInstallationUpdateOne { + giuo.mutation.SetAccountURL(s) + return giuo +} + +// SetNillableAccountURL sets the "account_url" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableAccountURL(s *string) *GithubInstallationUpdateOne { + if s != nil { + giuo.SetAccountURL(*s) + } + return giuo +} + +// SetRepositorySelection sets the "repository_selection" field. +func (giuo *GithubInstallationUpdateOne) SetRepositorySelection(gs githubinstallation.RepositorySelection) *GithubInstallationUpdateOne { + giuo.mutation.SetRepositorySelection(gs) + return giuo +} + +// SetNillableRepositorySelection sets the "repository_selection" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableRepositorySelection(gs *githubinstallation.RepositorySelection) *GithubInstallationUpdateOne { + if gs != nil { + giuo.SetRepositorySelection(*gs) + } + return giuo +} + +// SetSuspended sets the "suspended" field. +func (giuo *GithubInstallationUpdateOne) SetSuspended(b bool) *GithubInstallationUpdateOne { + giuo.mutation.SetSuspended(b) + return giuo +} + +// SetNillableSuspended sets the "suspended" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableSuspended(b *bool) *GithubInstallationUpdateOne { + if b != nil { + giuo.SetSuspended(*b) + } + return giuo +} + +// SetActive sets the "active" field. +func (giuo *GithubInstallationUpdateOne) SetActive(b bool) *GithubInstallationUpdateOne { + giuo.mutation.SetActive(b) + return giuo +} + +// SetNillableActive sets the "active" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillableActive(b *bool) *GithubInstallationUpdateOne { + if b != nil { + giuo.SetActive(*b) + } + return giuo +} + +// SetPermissions sets the "permissions" field. +func (giuo *GithubInstallationUpdateOne) SetPermissions(mip models.GithubInstallationPermissions) *GithubInstallationUpdateOne { + giuo.mutation.SetPermissions(mip) + return giuo +} + +// SetNillablePermissions sets the "permissions" field if the given value is not nil. +func (giuo *GithubInstallationUpdateOne) SetNillablePermissions(mip *models.GithubInstallationPermissions) *GithubInstallationUpdateOne { + if mip != nil { + giuo.SetPermissions(*mip) + } + return giuo +} + +// ClearPermissions clears the value of the "permissions" field. +func (giuo *GithubInstallationUpdateOne) ClearPermissions() *GithubInstallationUpdateOne { + giuo.mutation.ClearPermissions() + return giuo +} + +// SetEvents sets the "events" field. +func (giuo *GithubInstallationUpdateOne) SetEvents(s []string) *GithubInstallationUpdateOne { + giuo.mutation.SetEvents(s) + return giuo +} + +// AppendEvents appends s to the "events" field. +func (giuo *GithubInstallationUpdateOne) AppendEvents(s []string) *GithubInstallationUpdateOne { + giuo.mutation.AppendEvents(s) + return giuo +} + +// ClearEvents clears the value of the "events" field. +func (giuo *GithubInstallationUpdateOne) ClearEvents() *GithubInstallationUpdateOne { + giuo.mutation.ClearEvents() + return giuo +} + +// SetGithubAppsID sets the "github_apps" edge to the GithubApp entity by ID. +func (giuo *GithubInstallationUpdateOne) SetGithubAppsID(id int64) *GithubInstallationUpdateOne { + giuo.mutation.SetGithubAppsID(id) + return giuo +} + +// SetGithubApps sets the "github_apps" edge to the GithubApp entity. +func (giuo *GithubInstallationUpdateOne) SetGithubApps(g *GithubApp) *GithubInstallationUpdateOne { + return giuo.SetGithubAppsID(g.ID) +} + +// Mutation returns the GithubInstallationMutation object of the builder. +func (giuo *GithubInstallationUpdateOne) Mutation() *GithubInstallationMutation { + return giuo.mutation +} + +// ClearGithubApps clears the "github_apps" edge to the GithubApp entity. +func (giuo *GithubInstallationUpdateOne) ClearGithubApps() *GithubInstallationUpdateOne { + giuo.mutation.ClearGithubApps() + return giuo +} + +// Where appends a list predicates to the GithubInstallationUpdate builder. +func (giuo *GithubInstallationUpdateOne) Where(ps ...predicate.GithubInstallation) *GithubInstallationUpdateOne { + giuo.mutation.Where(ps...) + return giuo +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (giuo *GithubInstallationUpdateOne) Select(field string, fields ...string) *GithubInstallationUpdateOne { + giuo.fields = append([]string{field}, fields...) + return giuo +} + +// Save executes the query and returns the updated GithubInstallation entity. +func (giuo *GithubInstallationUpdateOne) Save(ctx context.Context) (*GithubInstallation, error) { + giuo.defaults() + return withHooks(ctx, giuo.sqlSave, giuo.mutation, giuo.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (giuo *GithubInstallationUpdateOne) SaveX(ctx context.Context) *GithubInstallation { + node, err := giuo.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (giuo *GithubInstallationUpdateOne) Exec(ctx context.Context) error { + _, err := giuo.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (giuo *GithubInstallationUpdateOne) ExecX(ctx context.Context) { + if err := giuo.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (giuo *GithubInstallationUpdateOne) defaults() { + if _, ok := giuo.mutation.UpdatedAt(); !ok { + v := githubinstallation.UpdateDefaultUpdatedAt() + giuo.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (giuo *GithubInstallationUpdateOne) check() error { + if v, ok := giuo.mutation.AccountLogin(); ok { + if err := githubinstallation.AccountLoginValidator(v); err != nil { + return &ValidationError{Name: "account_login", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_login": %w`, err)} + } + } + if v, ok := giuo.mutation.AccountType(); ok { + if err := githubinstallation.AccountTypeValidator(v); err != nil { + return &ValidationError{Name: "account_type", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_type": %w`, err)} + } + } + if v, ok := giuo.mutation.AccountURL(); ok { + if err := githubinstallation.AccountURLValidator(v); err != nil { + return &ValidationError{Name: "account_url", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.account_url": %w`, err)} + } + } + if v, ok := giuo.mutation.RepositorySelection(); ok { + if err := githubinstallation.RepositorySelectionValidator(v); err != nil { + return &ValidationError{Name: "repository_selection", err: fmt.Errorf(`ent: validator failed for field "GithubInstallation.repository_selection": %w`, err)} + } + } + if giuo.mutation.GithubAppsCleared() && len(giuo.mutation.GithubAppsIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "GithubInstallation.github_apps"`) + } + return nil +} + +// Modify adds a statement modifier for attaching custom logic to the UPDATE statement. +func (giuo *GithubInstallationUpdateOne) Modify(modifiers ...func(u *sql.UpdateBuilder)) *GithubInstallationUpdateOne { + giuo.modifiers = append(giuo.modifiers, modifiers...) + return giuo +} + +func (giuo *GithubInstallationUpdateOne) sqlSave(ctx context.Context) (_node *GithubInstallation, err error) { + if err := giuo.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(githubinstallation.Table, githubinstallation.Columns, sqlgraph.NewFieldSpec(githubinstallation.FieldID, field.TypeInt64)) + id, ok := giuo.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "GithubInstallation.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := giuo.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, githubinstallation.FieldID) + for _, f := range fields { + if !githubinstallation.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != githubinstallation.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := giuo.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := giuo.mutation.UpdatedAt(); ok { + _spec.SetField(githubinstallation.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := giuo.mutation.AccountID(); ok { + _spec.SetField(githubinstallation.FieldAccountID, field.TypeInt64, value) + } + if value, ok := giuo.mutation.AddedAccountID(); ok { + _spec.AddField(githubinstallation.FieldAccountID, field.TypeInt64, value) + } + if value, ok := giuo.mutation.AccountLogin(); ok { + _spec.SetField(githubinstallation.FieldAccountLogin, field.TypeString, value) + } + if value, ok := giuo.mutation.AccountType(); ok { + _spec.SetField(githubinstallation.FieldAccountType, field.TypeEnum, value) + } + if value, ok := giuo.mutation.AccountURL(); ok { + _spec.SetField(githubinstallation.FieldAccountURL, field.TypeString, value) + } + if value, ok := giuo.mutation.RepositorySelection(); ok { + _spec.SetField(githubinstallation.FieldRepositorySelection, field.TypeEnum, value) + } + if value, ok := giuo.mutation.Suspended(); ok { + _spec.SetField(githubinstallation.FieldSuspended, field.TypeBool, value) + } + if value, ok := giuo.mutation.Active(); ok { + _spec.SetField(githubinstallation.FieldActive, field.TypeBool, value) + } + if value, ok := giuo.mutation.Permissions(); ok { + _spec.SetField(githubinstallation.FieldPermissions, field.TypeJSON, value) + } + if giuo.mutation.PermissionsCleared() { + _spec.ClearField(githubinstallation.FieldPermissions, field.TypeJSON) + } + if value, ok := giuo.mutation.Events(); ok { + _spec.SetField(githubinstallation.FieldEvents, field.TypeJSON, value) + } + if value, ok := giuo.mutation.AppendedEvents(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, githubinstallation.FieldEvents, value) + }) + } + if giuo.mutation.EventsCleared() { + _spec.ClearField(githubinstallation.FieldEvents, field.TypeJSON) + } + if giuo.mutation.GithubAppsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: githubinstallation.GithubAppsTable, + Columns: []string{githubinstallation.GithubAppsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := giuo.mutation.GithubAppsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: githubinstallation.GithubAppsTable, + Columns: []string{githubinstallation.GithubAppsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(githubapp.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + _spec.AddModifiers(giuo.modifiers...) + _node = &GithubInstallation{config: giuo.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, giuo.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{githubinstallation.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + giuo.mutation.done = true + return _node, nil +} diff --git a/ent/hook/hook.go b/ent/hook/hook.go index df771012..eafe2997 100644 --- a/ent/hook/hook.go +++ b/ent/hook/hook.go @@ -21,6 +21,18 @@ func (f GithubAppFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, e return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GithubAppMutation", m) } +// The GithubInstallationFunc type is an adapter to allow the use of ordinary +// function as GithubInstallation mutator. +type GithubInstallationFunc func(context.Context, *ent.GithubInstallationMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f GithubInstallationFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.GithubInstallationMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.GithubInstallationMutation", m) +} + // The UserFunc type is an adapter to allow the use of ordinary // function as User mutator. type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error) diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 28b8a6bd..d3a93f3c 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -11,10 +11,9 @@ import ( var ( // GithubAppsColumns holds the columns for the "github_apps" table. GithubAppsColumns = []*schema.Column{ - {Name: "id", Type: field.TypeUUID, Unique: true}, + {Name: "id", Type: field.TypeInt64, Increment: true}, {Name: "created_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime}, - {Name: "github_app_id", Type: field.TypeInt64, Unique: true}, {Name: "name", Type: field.TypeString}, {Name: "client_id", Type: field.TypeString}, {Name: "client_secret", Type: field.TypeString}, @@ -26,11 +25,41 @@ var ( Name: "github_apps", Columns: GithubAppsColumns, PrimaryKey: []*schema.Column{GithubAppsColumns[0]}, + } + // GithubInstallationsColumns holds the columns for the "github_installations" table. + GithubInstallationsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeInt64, Increment: true}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "account_id", Type: field.TypeInt64}, + {Name: "account_login", Type: field.TypeString}, + {Name: "account_type", Type: field.TypeEnum, Enums: []string{"Organization", "User"}}, + {Name: "account_url", Type: field.TypeString}, + {Name: "repository_selection", Type: field.TypeEnum, Enums: []string{"all", "selected"}, Default: "all"}, + {Name: "suspended", Type: field.TypeBool, Default: false}, + {Name: "active", Type: field.TypeBool, Default: true}, + {Name: "permissions", Type: field.TypeJSON, Nullable: true}, + {Name: "events", Type: field.TypeJSON, Nullable: true}, + {Name: "github_app_id", Type: field.TypeInt64}, + } + // GithubInstallationsTable holds the schema information for the "github_installations" table. + GithubInstallationsTable = &schema.Table{ + Name: "github_installations", + Columns: GithubInstallationsColumns, + PrimaryKey: []*schema.Column{GithubInstallationsColumns[0]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "github_installations_github_apps_installations", + Columns: []*schema.Column{GithubInstallationsColumns[12]}, + RefColumns: []*schema.Column{GithubAppsColumns[0]}, + OnDelete: schema.Cascade, + }, + }, Indexes: []*schema.Index{ { - Name: "githubapp_github_app_id", + Name: "githubinstallation_github_app_id", Unique: true, - Columns: []*schema.Column{GithubAppsColumns[3]}, + Columns: []*schema.Column{GithubInstallationsColumns[12]}, }, }, } @@ -52,6 +81,7 @@ var ( // Tables holds all the tables in the schema. Tables = []*schema.Table{ GithubAppsTable, + GithubInstallationsTable, UsersTable, } ) @@ -60,6 +90,10 @@ func init() { GithubAppsTable.Annotation = &entsql.Annotation{ Table: "github_apps", } + GithubInstallationsTable.ForeignKeys[0].RefTable = GithubAppsTable + GithubInstallationsTable.Annotation = &entsql.Annotation{ + Table: "github_installations", + } UsersTable.Annotation = &entsql.Annotation{ Table: "users", } diff --git a/ent/mutation.go b/ent/mutation.go index 1b512508..7cd00442 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -13,8 +13,10 @@ import ( "entgo.io/ent/dialect/sql" "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/predicate" "github.com/unbindapp/unbind-api/ent/user" + "github.com/unbindapp/unbind-api/internal/models" ) const ( @@ -26,29 +28,31 @@ const ( OpUpdateOne = ent.OpUpdateOne // Node types. - TypeGithubApp = "GithubApp" - TypeUser = "User" + TypeGithubApp = "GithubApp" + TypeGithubInstallation = "GithubInstallation" + TypeUser = "User" ) // GithubAppMutation represents an operation that mutates the GithubApp nodes in the graph. type GithubAppMutation struct { config - op Op - typ string - id *uuid.UUID - created_at *time.Time - updated_at *time.Time - github_app_id *int64 - addgithub_app_id *int64 - name *string - client_id *string - client_secret *string - webhook_secret *string - private_key *string - clearedFields map[string]struct{} - done bool - oldValue func(context.Context) (*GithubApp, error) - predicates []predicate.GithubApp + op Op + typ string + id *int64 + created_at *time.Time + updated_at *time.Time + name *string + client_id *string + client_secret *string + webhook_secret *string + private_key *string + clearedFields map[string]struct{} + installations map[int64]struct{} + removedinstallations map[int64]struct{} + clearedinstallations bool + done bool + oldValue func(context.Context) (*GithubApp, error) + predicates []predicate.GithubApp } var _ ent.Mutation = (*GithubAppMutation)(nil) @@ -71,7 +75,7 @@ func newGithubAppMutation(c config, op Op, opts ...githubappOption) *GithubAppMu } // withGithubAppID sets the ID field of the mutation. -func withGithubAppID(id uuid.UUID) githubappOption { +func withGithubAppID(id int64) githubappOption { return func(m *GithubAppMutation) { var ( err error @@ -123,13 +127,13 @@ func (m GithubAppMutation) Tx() (*Tx, error) { // SetID sets the value of the id field. Note that this // operation is only accepted on creation of GithubApp entities. -func (m *GithubAppMutation) SetID(id uuid.UUID) { +func (m *GithubAppMutation) SetID(id int64) { m.id = &id } // ID returns the ID value in the mutation. Note that the ID is only available // if it was provided to the builder or after it was returned from the database. -func (m *GithubAppMutation) ID() (id uuid.UUID, exists bool) { +func (m *GithubAppMutation) ID() (id int64, exists bool) { if m.id == nil { return } @@ -140,12 +144,12 @@ func (m *GithubAppMutation) ID() (id uuid.UUID, exists bool) { // That means, if the mutation is applied within a transaction with an isolation level such // as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated // or updated by the mutation. -func (m *GithubAppMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { +func (m *GithubAppMutation) IDs(ctx context.Context) ([]int64, error) { switch { case m.op.Is(OpUpdateOne | OpDeleteOne): id, exists := m.ID() if exists { - return []uuid.UUID{id}, nil + return []int64{id}, nil } fallthrough case m.op.Is(OpUpdate | OpDelete): @@ -227,62 +231,6 @@ func (m *GithubAppMutation) ResetUpdatedAt() { m.updated_at = nil } -// SetGithubAppID sets the "github_app_id" field. -func (m *GithubAppMutation) SetGithubAppID(i int64) { - m.github_app_id = &i - m.addgithub_app_id = nil -} - -// GithubAppID returns the value of the "github_app_id" field in the mutation. -func (m *GithubAppMutation) GithubAppID() (r int64, exists bool) { - v := m.github_app_id - if v == nil { - return - } - return *v, true -} - -// OldGithubAppID returns the old "github_app_id" field's value of the GithubApp entity. -// If the GithubApp object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *GithubAppMutation) OldGithubAppID(ctx context.Context) (v int64, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldGithubAppID is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldGithubAppID requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldGithubAppID: %w", err) - } - return oldValue.GithubAppID, nil -} - -// AddGithubAppID adds i to the "github_app_id" field. -func (m *GithubAppMutation) AddGithubAppID(i int64) { - if m.addgithub_app_id != nil { - *m.addgithub_app_id += i - } else { - m.addgithub_app_id = &i - } -} - -// AddedGithubAppID returns the value that was added to the "github_app_id" field in this mutation. -func (m *GithubAppMutation) AddedGithubAppID() (r int64, exists bool) { - v := m.addgithub_app_id - if v == nil { - return - } - return *v, true -} - -// ResetGithubAppID resets all changes to the "github_app_id" field. -func (m *GithubAppMutation) ResetGithubAppID() { - m.github_app_id = nil - m.addgithub_app_id = nil -} - // SetName sets the "name" field. func (m *GithubAppMutation) SetName(s string) { m.name = &s @@ -463,6 +411,60 @@ func (m *GithubAppMutation) ResetPrivateKey() { m.private_key = nil } +// AddInstallationIDs adds the "installations" edge to the GithubInstallation entity by ids. +func (m *GithubAppMutation) AddInstallationIDs(ids ...int64) { + if m.installations == nil { + m.installations = make(map[int64]struct{}) + } + for i := range ids { + m.installations[ids[i]] = struct{}{} + } +} + +// ClearInstallations clears the "installations" edge to the GithubInstallation entity. +func (m *GithubAppMutation) ClearInstallations() { + m.clearedinstallations = true +} + +// InstallationsCleared reports if the "installations" edge to the GithubInstallation entity was cleared. +func (m *GithubAppMutation) InstallationsCleared() bool { + return m.clearedinstallations +} + +// RemoveInstallationIDs removes the "installations" edge to the GithubInstallation entity by IDs. +func (m *GithubAppMutation) RemoveInstallationIDs(ids ...int64) { + if m.removedinstallations == nil { + m.removedinstallations = make(map[int64]struct{}) + } + for i := range ids { + delete(m.installations, ids[i]) + m.removedinstallations[ids[i]] = struct{}{} + } +} + +// RemovedInstallations returns the removed IDs of the "installations" edge to the GithubInstallation entity. +func (m *GithubAppMutation) RemovedInstallationsIDs() (ids []int64) { + for id := range m.removedinstallations { + ids = append(ids, id) + } + return +} + +// InstallationsIDs returns the "installations" edge IDs in the mutation. +func (m *GithubAppMutation) InstallationsIDs() (ids []int64) { + for id := range m.installations { + ids = append(ids, id) + } + return +} + +// ResetInstallations resets all changes to the "installations" edge. +func (m *GithubAppMutation) ResetInstallations() { + m.installations = nil + m.clearedinstallations = false + m.removedinstallations = nil +} + // Where appends a list predicates to the GithubAppMutation builder. func (m *GithubAppMutation) Where(ps ...predicate.GithubApp) { m.predicates = append(m.predicates, ps...) @@ -497,16 +499,13 @@ func (m *GithubAppMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *GithubAppMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 7) if m.created_at != nil { fields = append(fields, githubapp.FieldCreatedAt) } if m.updated_at != nil { fields = append(fields, githubapp.FieldUpdatedAt) } - if m.github_app_id != nil { - fields = append(fields, githubapp.FieldGithubAppID) - } if m.name != nil { fields = append(fields, githubapp.FieldName) } @@ -534,8 +533,6 @@ func (m *GithubAppMutation) Field(name string) (ent.Value, bool) { return m.CreatedAt() case githubapp.FieldUpdatedAt: return m.UpdatedAt() - case githubapp.FieldGithubAppID: - return m.GithubAppID() case githubapp.FieldName: return m.Name() case githubapp.FieldClientID: @@ -559,8 +556,6 @@ func (m *GithubAppMutation) OldField(ctx context.Context, name string) (ent.Valu return m.OldCreatedAt(ctx) case githubapp.FieldUpdatedAt: return m.OldUpdatedAt(ctx) - case githubapp.FieldGithubAppID: - return m.OldGithubAppID(ctx) case githubapp.FieldName: return m.OldName(ctx) case githubapp.FieldClientID: @@ -594,13 +589,6 @@ func (m *GithubAppMutation) SetField(name string, value ent.Value) error { } m.SetUpdatedAt(v) return nil - case githubapp.FieldGithubAppID: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetGithubAppID(v) - return nil case githubapp.FieldName: v, ok := value.(string) if !ok { @@ -643,21 +631,13 @@ func (m *GithubAppMutation) SetField(name string, value ent.Value) error { // AddedFields returns all numeric fields that were incremented/decremented during // this mutation. func (m *GithubAppMutation) AddedFields() []string { - var fields []string - if m.addgithub_app_id != nil { - fields = append(fields, githubapp.FieldGithubAppID) - } - return fields + return nil } // AddedField returns the numeric value that was incremented/decremented on a field // with the given name. The second boolean return value indicates that this field // was not set, or was not defined in the schema. func (m *GithubAppMutation) AddedField(name string) (ent.Value, bool) { - switch name { - case githubapp.FieldGithubAppID: - return m.AddedGithubAppID() - } return nil, false } @@ -666,13 +646,6 @@ func (m *GithubAppMutation) AddedField(name string) (ent.Value, bool) { // type. func (m *GithubAppMutation) AddField(name string, value ent.Value) error { switch name { - case githubapp.FieldGithubAppID: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddGithubAppID(v) - return nil } return fmt.Errorf("unknown GithubApp numeric field %s", name) } @@ -706,9 +679,6 @@ func (m *GithubAppMutation) ResetField(name string) error { case githubapp.FieldUpdatedAt: m.ResetUpdatedAt() return nil - case githubapp.FieldGithubAppID: - m.ResetGithubAppID() - return nil case githubapp.FieldName: m.ResetName() return nil @@ -730,52 +700,1175 @@ func (m *GithubAppMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *GithubAppMutation) AddedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.installations != nil { + edges = append(edges, githubapp.EdgeInstallations) + } return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. func (m *GithubAppMutation) AddedIDs(name string) []ent.Value { + switch name { + case githubapp.EdgeInstallations: + ids := make([]ent.Value, 0, len(m.installations)) + for id := range m.installations { + ids = append(ids, id) + } + return ids + } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *GithubAppMutation) RemovedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.removedinstallations != nil { + edges = append(edges, githubapp.EdgeInstallations) + } return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. func (m *GithubAppMutation) RemovedIDs(name string) []ent.Value { + switch name { + case githubapp.EdgeInstallations: + ids := make([]ent.Value, 0, len(m.removedinstallations)) + for id := range m.removedinstallations { + ids = append(ids, id) + } + return ids + } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *GithubAppMutation) ClearedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.clearedinstallations { + edges = append(edges, githubapp.EdgeInstallations) + } return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. func (m *GithubAppMutation) EdgeCleared(name string) bool { + switch name { + case githubapp.EdgeInstallations: + return m.clearedinstallations + } return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. func (m *GithubAppMutation) ClearEdge(name string) error { + switch name { + } return fmt.Errorf("unknown GithubApp unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. func (m *GithubAppMutation) ResetEdge(name string) error { + switch name { + case githubapp.EdgeInstallations: + m.ResetInstallations() + return nil + } return fmt.Errorf("unknown GithubApp edge %s", name) } +// GithubInstallationMutation represents an operation that mutates the GithubInstallation nodes in the graph. +type GithubInstallationMutation struct { + config + op Op + typ string + id *int64 + created_at *time.Time + updated_at *time.Time + account_id *int64 + addaccount_id *int64 + account_login *string + account_type *githubinstallation.AccountType + account_url *string + repository_selection *githubinstallation.RepositorySelection + suspended *bool + active *bool + permissions *models.GithubInstallationPermissions + events *[]string + appendevents []string + clearedFields map[string]struct{} + github_apps *int64 + clearedgithub_apps bool + done bool + oldValue func(context.Context) (*GithubInstallation, error) + predicates []predicate.GithubInstallation +} + +var _ ent.Mutation = (*GithubInstallationMutation)(nil) + +// githubinstallationOption allows management of the mutation configuration using functional options. +type githubinstallationOption func(*GithubInstallationMutation) + +// newGithubInstallationMutation creates new mutation for the GithubInstallation entity. +func newGithubInstallationMutation(c config, op Op, opts ...githubinstallationOption) *GithubInstallationMutation { + m := &GithubInstallationMutation{ + config: c, + op: op, + typ: TypeGithubInstallation, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withGithubInstallationID sets the ID field of the mutation. +func withGithubInstallationID(id int64) githubinstallationOption { + return func(m *GithubInstallationMutation) { + var ( + err error + once sync.Once + value *GithubInstallation + ) + m.oldValue = func(ctx context.Context) (*GithubInstallation, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().GithubInstallation.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withGithubInstallation sets the old GithubInstallation of the mutation. +func withGithubInstallation(node *GithubInstallation) githubinstallationOption { + return func(m *GithubInstallationMutation) { + m.oldValue = func(context.Context) (*GithubInstallation, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m GithubInstallationMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m GithubInstallationMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of GithubInstallation entities. +func (m *GithubInstallationMutation) SetID(id int64) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *GithubInstallationMutation) ID() (id int64, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *GithubInstallationMutation) IDs(ctx context.Context) ([]int64, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []int64{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().GithubInstallation.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetCreatedAt sets the "created_at" field. +func (m *GithubInstallationMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *GithubInstallationMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *GithubInstallationMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *GithubInstallationMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *GithubInstallationMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *GithubInstallationMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetGithubAppID sets the "github_app_id" field. +func (m *GithubInstallationMutation) SetGithubAppID(i int64) { + m.github_apps = &i +} + +// GithubAppID returns the value of the "github_app_id" field in the mutation. +func (m *GithubInstallationMutation) GithubAppID() (r int64, exists bool) { + v := m.github_apps + if v == nil { + return + } + return *v, true +} + +// OldGithubAppID returns the old "github_app_id" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldGithubAppID(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldGithubAppID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldGithubAppID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldGithubAppID: %w", err) + } + return oldValue.GithubAppID, nil +} + +// ResetGithubAppID resets all changes to the "github_app_id" field. +func (m *GithubInstallationMutation) ResetGithubAppID() { + m.github_apps = nil +} + +// SetAccountID sets the "account_id" field. +func (m *GithubInstallationMutation) SetAccountID(i int64) { + m.account_id = &i + m.addaccount_id = nil +} + +// AccountID returns the value of the "account_id" field in the mutation. +func (m *GithubInstallationMutation) AccountID() (r int64, exists bool) { + v := m.account_id + if v == nil { + return + } + return *v, true +} + +// OldAccountID returns the old "account_id" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountID(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountID: %w", err) + } + return oldValue.AccountID, nil +} + +// AddAccountID adds i to the "account_id" field. +func (m *GithubInstallationMutation) AddAccountID(i int64) { + if m.addaccount_id != nil { + *m.addaccount_id += i + } else { + m.addaccount_id = &i + } +} + +// AddedAccountID returns the value that was added to the "account_id" field in this mutation. +func (m *GithubInstallationMutation) AddedAccountID() (r int64, exists bool) { + v := m.addaccount_id + if v == nil { + return + } + return *v, true +} + +// ResetAccountID resets all changes to the "account_id" field. +func (m *GithubInstallationMutation) ResetAccountID() { + m.account_id = nil + m.addaccount_id = nil +} + +// SetAccountLogin sets the "account_login" field. +func (m *GithubInstallationMutation) SetAccountLogin(s string) { + m.account_login = &s +} + +// AccountLogin returns the value of the "account_login" field in the mutation. +func (m *GithubInstallationMutation) AccountLogin() (r string, exists bool) { + v := m.account_login + if v == nil { + return + } + return *v, true +} + +// OldAccountLogin returns the old "account_login" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountLogin(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountLogin is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountLogin requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountLogin: %w", err) + } + return oldValue.AccountLogin, nil +} + +// ResetAccountLogin resets all changes to the "account_login" field. +func (m *GithubInstallationMutation) ResetAccountLogin() { + m.account_login = nil +} + +// SetAccountType sets the "account_type" field. +func (m *GithubInstallationMutation) SetAccountType(gt githubinstallation.AccountType) { + m.account_type = > +} + +// AccountType returns the value of the "account_type" field in the mutation. +func (m *GithubInstallationMutation) AccountType() (r githubinstallation.AccountType, exists bool) { + v := m.account_type + if v == nil { + return + } + return *v, true +} + +// OldAccountType returns the old "account_type" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountType(ctx context.Context) (v githubinstallation.AccountType, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountType: %w", err) + } + return oldValue.AccountType, nil +} + +// ResetAccountType resets all changes to the "account_type" field. +func (m *GithubInstallationMutation) ResetAccountType() { + m.account_type = nil +} + +// SetAccountURL sets the "account_url" field. +func (m *GithubInstallationMutation) SetAccountURL(s string) { + m.account_url = &s +} + +// AccountURL returns the value of the "account_url" field in the mutation. +func (m *GithubInstallationMutation) AccountURL() (r string, exists bool) { + v := m.account_url + if v == nil { + return + } + return *v, true +} + +// OldAccountURL returns the old "account_url" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldAccountURL(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAccountURL is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAccountURL requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAccountURL: %w", err) + } + return oldValue.AccountURL, nil +} + +// ResetAccountURL resets all changes to the "account_url" field. +func (m *GithubInstallationMutation) ResetAccountURL() { + m.account_url = nil +} + +// SetRepositorySelection sets the "repository_selection" field. +func (m *GithubInstallationMutation) SetRepositorySelection(gs githubinstallation.RepositorySelection) { + m.repository_selection = &gs +} + +// RepositorySelection returns the value of the "repository_selection" field in the mutation. +func (m *GithubInstallationMutation) RepositorySelection() (r githubinstallation.RepositorySelection, exists bool) { + v := m.repository_selection + if v == nil { + return + } + return *v, true +} + +// OldRepositorySelection returns the old "repository_selection" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldRepositorySelection(ctx context.Context) (v githubinstallation.RepositorySelection, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRepositorySelection is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRepositorySelection requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRepositorySelection: %w", err) + } + return oldValue.RepositorySelection, nil +} + +// ResetRepositorySelection resets all changes to the "repository_selection" field. +func (m *GithubInstallationMutation) ResetRepositorySelection() { + m.repository_selection = nil +} + +// SetSuspended sets the "suspended" field. +func (m *GithubInstallationMutation) SetSuspended(b bool) { + m.suspended = &b +} + +// Suspended returns the value of the "suspended" field in the mutation. +func (m *GithubInstallationMutation) Suspended() (r bool, exists bool) { + v := m.suspended + if v == nil { + return + } + return *v, true +} + +// OldSuspended returns the old "suspended" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldSuspended(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSuspended is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSuspended requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSuspended: %w", err) + } + return oldValue.Suspended, nil +} + +// ResetSuspended resets all changes to the "suspended" field. +func (m *GithubInstallationMutation) ResetSuspended() { + m.suspended = nil +} + +// SetActive sets the "active" field. +func (m *GithubInstallationMutation) SetActive(b bool) { + m.active = &b +} + +// Active returns the value of the "active" field in the mutation. +func (m *GithubInstallationMutation) Active() (r bool, exists bool) { + v := m.active + if v == nil { + return + } + return *v, true +} + +// OldActive returns the old "active" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldActive(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldActive is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldActive requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldActive: %w", err) + } + return oldValue.Active, nil +} + +// ResetActive resets all changes to the "active" field. +func (m *GithubInstallationMutation) ResetActive() { + m.active = nil +} + +// SetPermissions sets the "permissions" field. +func (m *GithubInstallationMutation) SetPermissions(mip models.GithubInstallationPermissions) { + m.permissions = &mip +} + +// Permissions returns the value of the "permissions" field in the mutation. +func (m *GithubInstallationMutation) Permissions() (r models.GithubInstallationPermissions, exists bool) { + v := m.permissions + if v == nil { + return + } + return *v, true +} + +// OldPermissions returns the old "permissions" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldPermissions(ctx context.Context) (v models.GithubInstallationPermissions, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPermissions is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPermissions requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPermissions: %w", err) + } + return oldValue.Permissions, nil +} + +// ClearPermissions clears the value of the "permissions" field. +func (m *GithubInstallationMutation) ClearPermissions() { + m.permissions = nil + m.clearedFields[githubinstallation.FieldPermissions] = struct{}{} +} + +// PermissionsCleared returns if the "permissions" field was cleared in this mutation. +func (m *GithubInstallationMutation) PermissionsCleared() bool { + _, ok := m.clearedFields[githubinstallation.FieldPermissions] + return ok +} + +// ResetPermissions resets all changes to the "permissions" field. +func (m *GithubInstallationMutation) ResetPermissions() { + m.permissions = nil + delete(m.clearedFields, githubinstallation.FieldPermissions) +} + +// SetEvents sets the "events" field. +func (m *GithubInstallationMutation) SetEvents(s []string) { + m.events = &s + m.appendevents = nil +} + +// Events returns the value of the "events" field in the mutation. +func (m *GithubInstallationMutation) Events() (r []string, exists bool) { + v := m.events + if v == nil { + return + } + return *v, true +} + +// OldEvents returns the old "events" field's value of the GithubInstallation entity. +// If the GithubInstallation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GithubInstallationMutation) OldEvents(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEvents is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEvents requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEvents: %w", err) + } + return oldValue.Events, nil +} + +// AppendEvents adds s to the "events" field. +func (m *GithubInstallationMutation) AppendEvents(s []string) { + m.appendevents = append(m.appendevents, s...) +} + +// AppendedEvents returns the list of values that were appended to the "events" field in this mutation. +func (m *GithubInstallationMutation) AppendedEvents() ([]string, bool) { + if len(m.appendevents) == 0 { + return nil, false + } + return m.appendevents, true +} + +// ClearEvents clears the value of the "events" field. +func (m *GithubInstallationMutation) ClearEvents() { + m.events = nil + m.appendevents = nil + m.clearedFields[githubinstallation.FieldEvents] = struct{}{} +} + +// EventsCleared returns if the "events" field was cleared in this mutation. +func (m *GithubInstallationMutation) EventsCleared() bool { + _, ok := m.clearedFields[githubinstallation.FieldEvents] + return ok +} + +// ResetEvents resets all changes to the "events" field. +func (m *GithubInstallationMutation) ResetEvents() { + m.events = nil + m.appendevents = nil + delete(m.clearedFields, githubinstallation.FieldEvents) +} + +// SetGithubAppsID sets the "github_apps" edge to the GithubApp entity by id. +func (m *GithubInstallationMutation) SetGithubAppsID(id int64) { + m.github_apps = &id +} + +// ClearGithubApps clears the "github_apps" edge to the GithubApp entity. +func (m *GithubInstallationMutation) ClearGithubApps() { + m.clearedgithub_apps = true + m.clearedFields[githubinstallation.FieldGithubAppID] = struct{}{} +} + +// GithubAppsCleared reports if the "github_apps" edge to the GithubApp entity was cleared. +func (m *GithubInstallationMutation) GithubAppsCleared() bool { + return m.clearedgithub_apps +} + +// GithubAppsID returns the "github_apps" edge ID in the mutation. +func (m *GithubInstallationMutation) GithubAppsID() (id int64, exists bool) { + if m.github_apps != nil { + return *m.github_apps, true + } + return +} + +// GithubAppsIDs returns the "github_apps" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// GithubAppsID instead. It exists only for internal usage by the builders. +func (m *GithubInstallationMutation) GithubAppsIDs() (ids []int64) { + if id := m.github_apps; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetGithubApps resets all changes to the "github_apps" edge. +func (m *GithubInstallationMutation) ResetGithubApps() { + m.github_apps = nil + m.clearedgithub_apps = false +} + +// Where appends a list predicates to the GithubInstallationMutation builder. +func (m *GithubInstallationMutation) Where(ps ...predicate.GithubInstallation) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the GithubInstallationMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *GithubInstallationMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.GithubInstallation, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *GithubInstallationMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *GithubInstallationMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (GithubInstallation). +func (m *GithubInstallationMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *GithubInstallationMutation) Fields() []string { + fields := make([]string, 0, 12) + if m.created_at != nil { + fields = append(fields, githubinstallation.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, githubinstallation.FieldUpdatedAt) + } + if m.github_apps != nil { + fields = append(fields, githubinstallation.FieldGithubAppID) + } + if m.account_id != nil { + fields = append(fields, githubinstallation.FieldAccountID) + } + if m.account_login != nil { + fields = append(fields, githubinstallation.FieldAccountLogin) + } + if m.account_type != nil { + fields = append(fields, githubinstallation.FieldAccountType) + } + if m.account_url != nil { + fields = append(fields, githubinstallation.FieldAccountURL) + } + if m.repository_selection != nil { + fields = append(fields, githubinstallation.FieldRepositorySelection) + } + if m.suspended != nil { + fields = append(fields, githubinstallation.FieldSuspended) + } + if m.active != nil { + fields = append(fields, githubinstallation.FieldActive) + } + if m.permissions != nil { + fields = append(fields, githubinstallation.FieldPermissions) + } + if m.events != nil { + fields = append(fields, githubinstallation.FieldEvents) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *GithubInstallationMutation) Field(name string) (ent.Value, bool) { + switch name { + case githubinstallation.FieldCreatedAt: + return m.CreatedAt() + case githubinstallation.FieldUpdatedAt: + return m.UpdatedAt() + case githubinstallation.FieldGithubAppID: + return m.GithubAppID() + case githubinstallation.FieldAccountID: + return m.AccountID() + case githubinstallation.FieldAccountLogin: + return m.AccountLogin() + case githubinstallation.FieldAccountType: + return m.AccountType() + case githubinstallation.FieldAccountURL: + return m.AccountURL() + case githubinstallation.FieldRepositorySelection: + return m.RepositorySelection() + case githubinstallation.FieldSuspended: + return m.Suspended() + case githubinstallation.FieldActive: + return m.Active() + case githubinstallation.FieldPermissions: + return m.Permissions() + case githubinstallation.FieldEvents: + return m.Events() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *GithubInstallationMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case githubinstallation.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case githubinstallation.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case githubinstallation.FieldGithubAppID: + return m.OldGithubAppID(ctx) + case githubinstallation.FieldAccountID: + return m.OldAccountID(ctx) + case githubinstallation.FieldAccountLogin: + return m.OldAccountLogin(ctx) + case githubinstallation.FieldAccountType: + return m.OldAccountType(ctx) + case githubinstallation.FieldAccountURL: + return m.OldAccountURL(ctx) + case githubinstallation.FieldRepositorySelection: + return m.OldRepositorySelection(ctx) + case githubinstallation.FieldSuspended: + return m.OldSuspended(ctx) + case githubinstallation.FieldActive: + return m.OldActive(ctx) + case githubinstallation.FieldPermissions: + return m.OldPermissions(ctx) + case githubinstallation.FieldEvents: + return m.OldEvents(ctx) + } + return nil, fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubInstallationMutation) SetField(name string, value ent.Value) error { + switch name { + case githubinstallation.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case githubinstallation.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case githubinstallation.FieldGithubAppID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetGithubAppID(v) + return nil + case githubinstallation.FieldAccountID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountID(v) + return nil + case githubinstallation.FieldAccountLogin: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountLogin(v) + return nil + case githubinstallation.FieldAccountType: + v, ok := value.(githubinstallation.AccountType) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountType(v) + return nil + case githubinstallation.FieldAccountURL: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAccountURL(v) + return nil + case githubinstallation.FieldRepositorySelection: + v, ok := value.(githubinstallation.RepositorySelection) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRepositorySelection(v) + return nil + case githubinstallation.FieldSuspended: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSuspended(v) + return nil + case githubinstallation.FieldActive: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetActive(v) + return nil + case githubinstallation.FieldPermissions: + v, ok := value.(models.GithubInstallationPermissions) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPermissions(v) + return nil + case githubinstallation.FieldEvents: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEvents(v) + return nil + } + return fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *GithubInstallationMutation) AddedFields() []string { + var fields []string + if m.addaccount_id != nil { + fields = append(fields, githubinstallation.FieldAccountID) + } + return fields +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *GithubInstallationMutation) AddedField(name string) (ent.Value, bool) { + switch name { + case githubinstallation.FieldAccountID: + return m.AddedAccountID() + } + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *GithubInstallationMutation) AddField(name string, value ent.Value) error { + switch name { + case githubinstallation.FieldAccountID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAccountID(v) + return nil + } + return fmt.Errorf("unknown GithubInstallation numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *GithubInstallationMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(githubinstallation.FieldPermissions) { + fields = append(fields, githubinstallation.FieldPermissions) + } + if m.FieldCleared(githubinstallation.FieldEvents) { + fields = append(fields, githubinstallation.FieldEvents) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *GithubInstallationMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *GithubInstallationMutation) ClearField(name string) error { + switch name { + case githubinstallation.FieldPermissions: + m.ClearPermissions() + return nil + case githubinstallation.FieldEvents: + m.ClearEvents() + return nil + } + return fmt.Errorf("unknown GithubInstallation nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *GithubInstallationMutation) ResetField(name string) error { + switch name { + case githubinstallation.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case githubinstallation.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case githubinstallation.FieldGithubAppID: + m.ResetGithubAppID() + return nil + case githubinstallation.FieldAccountID: + m.ResetAccountID() + return nil + case githubinstallation.FieldAccountLogin: + m.ResetAccountLogin() + return nil + case githubinstallation.FieldAccountType: + m.ResetAccountType() + return nil + case githubinstallation.FieldAccountURL: + m.ResetAccountURL() + return nil + case githubinstallation.FieldRepositorySelection: + m.ResetRepositorySelection() + return nil + case githubinstallation.FieldSuspended: + m.ResetSuspended() + return nil + case githubinstallation.FieldActive: + m.ResetActive() + return nil + case githubinstallation.FieldPermissions: + m.ResetPermissions() + return nil + case githubinstallation.FieldEvents: + m.ResetEvents() + return nil + } + return fmt.Errorf("unknown GithubInstallation field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *GithubInstallationMutation) AddedEdges() []string { + edges := make([]string, 0, 1) + if m.github_apps != nil { + edges = append(edges, githubinstallation.EdgeGithubApps) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *GithubInstallationMutation) AddedIDs(name string) []ent.Value { + switch name { + case githubinstallation.EdgeGithubApps: + if id := m.github_apps; id != nil { + return []ent.Value{*id} + } + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *GithubInstallationMutation) RemovedEdges() []string { + edges := make([]string, 0, 1) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *GithubInstallationMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *GithubInstallationMutation) ClearedEdges() []string { + edges := make([]string, 0, 1) + if m.clearedgithub_apps { + edges = append(edges, githubinstallation.EdgeGithubApps) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *GithubInstallationMutation) EdgeCleared(name string) bool { + switch name { + case githubinstallation.EdgeGithubApps: + return m.clearedgithub_apps + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *GithubInstallationMutation) ClearEdge(name string) error { + switch name { + case githubinstallation.EdgeGithubApps: + m.ClearGithubApps() + return nil + } + return fmt.Errorf("unknown GithubInstallation unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *GithubInstallationMutation) ResetEdge(name string) error { + switch name { + case githubinstallation.EdgeGithubApps: + m.ResetGithubApps() + return nil + } + return fmt.Errorf("unknown GithubInstallation edge %s", name) +} + // UserMutation represents an operation that mutates the User nodes in the graph. type UserMutation struct { config diff --git a/ent/predicate/predicate.go b/ent/predicate/predicate.go index 022907f7..672795c6 100644 --- a/ent/predicate/predicate.go +++ b/ent/predicate/predicate.go @@ -9,5 +9,8 @@ import ( // GithubApp is the predicate function for githubapp builders. type GithubApp func(*sql.Selector) +// GithubInstallation is the predicate function for githubinstallation builders. +type GithubInstallation func(*sql.Selector) + // User is the predicate function for user builders. type User func(*sql.Selector) diff --git a/ent/runtime.go b/ent/runtime.go index 133421c8..45fbc255 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent/githubapp" + "github.com/unbindapp/unbind-api/ent/githubinstallation" "github.com/unbindapp/unbind-api/ent/schema" "github.com/unbindapp/unbind-api/ent/user" ) @@ -18,16 +19,14 @@ func init() { githubappMixin := schema.GithubApp{}.Mixin() githubappMixinFields0 := githubappMixin[0].Fields() _ = githubappMixinFields0 - githubappMixinFields1 := githubappMixin[1].Fields() - _ = githubappMixinFields1 githubappFields := schema.GithubApp{}.Fields() _ = githubappFields // githubappDescCreatedAt is the schema descriptor for created_at field. - githubappDescCreatedAt := githubappMixinFields1[0].Descriptor() + githubappDescCreatedAt := githubappMixinFields0[0].Descriptor() // githubapp.DefaultCreatedAt holds the default value on creation for the created_at field. githubapp.DefaultCreatedAt = githubappDescCreatedAt.Default.(func() time.Time) // githubappDescUpdatedAt is the schema descriptor for updated_at field. - githubappDescUpdatedAt := githubappMixinFields1[1].Descriptor() + githubappDescUpdatedAt := githubappMixinFields0[1].Descriptor() // githubapp.DefaultUpdatedAt holds the default value on creation for the updated_at field. githubapp.DefaultUpdatedAt = githubappDescUpdatedAt.Default.(func() time.Time) // githubapp.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. @@ -37,9 +36,44 @@ func init() { // githubapp.NameValidator is a validator for the "name" field. It is called by the builders before save. githubapp.NameValidator = githubappDescName.Validators[0].(func(string) error) // githubappDescID is the schema descriptor for id field. - githubappDescID := githubappMixinFields0[0].Descriptor() - // githubapp.DefaultID holds the default value on creation for the id field. - githubapp.DefaultID = githubappDescID.Default.(func() uuid.UUID) + githubappDescID := githubappFields[0].Descriptor() + // githubapp.IDValidator is a validator for the "id" field. It is called by the builders before save. + githubapp.IDValidator = githubappDescID.Validators[0].(func(int64) error) + githubinstallationMixin := schema.GithubInstallation{}.Mixin() + githubinstallationMixinFields0 := githubinstallationMixin[0].Fields() + _ = githubinstallationMixinFields0 + githubinstallationFields := schema.GithubInstallation{}.Fields() + _ = githubinstallationFields + // githubinstallationDescCreatedAt is the schema descriptor for created_at field. + githubinstallationDescCreatedAt := githubinstallationMixinFields0[0].Descriptor() + // githubinstallation.DefaultCreatedAt holds the default value on creation for the created_at field. + githubinstallation.DefaultCreatedAt = githubinstallationDescCreatedAt.Default.(func() time.Time) + // githubinstallationDescUpdatedAt is the schema descriptor for updated_at field. + githubinstallationDescUpdatedAt := githubinstallationMixinFields0[1].Descriptor() + // githubinstallation.DefaultUpdatedAt holds the default value on creation for the updated_at field. + githubinstallation.DefaultUpdatedAt = githubinstallationDescUpdatedAt.Default.(func() time.Time) + // githubinstallation.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + githubinstallation.UpdateDefaultUpdatedAt = githubinstallationDescUpdatedAt.UpdateDefault.(func() time.Time) + // githubinstallationDescAccountLogin is the schema descriptor for account_login field. + githubinstallationDescAccountLogin := githubinstallationFields[3].Descriptor() + // githubinstallation.AccountLoginValidator is a validator for the "account_login" field. It is called by the builders before save. + githubinstallation.AccountLoginValidator = githubinstallationDescAccountLogin.Validators[0].(func(string) error) + // githubinstallationDescAccountURL is the schema descriptor for account_url field. + githubinstallationDescAccountURL := githubinstallationFields[5].Descriptor() + // githubinstallation.AccountURLValidator is a validator for the "account_url" field. It is called by the builders before save. + githubinstallation.AccountURLValidator = githubinstallationDescAccountURL.Validators[0].(func(string) error) + // githubinstallationDescSuspended is the schema descriptor for suspended field. + githubinstallationDescSuspended := githubinstallationFields[7].Descriptor() + // githubinstallation.DefaultSuspended holds the default value on creation for the suspended field. + githubinstallation.DefaultSuspended = githubinstallationDescSuspended.Default.(bool) + // githubinstallationDescActive is the schema descriptor for active field. + githubinstallationDescActive := githubinstallationFields[8].Descriptor() + // githubinstallation.DefaultActive holds the default value on creation for the active field. + githubinstallation.DefaultActive = githubinstallationDescActive.Default.(bool) + // githubinstallationDescID is the schema descriptor for id field. + githubinstallationDescID := githubinstallationFields[0].Descriptor() + // githubinstallation.IDValidator is a validator for the "id" field. It is called by the builders before save. + githubinstallation.IDValidator = githubinstallationDescID.Validators[0].(func(int64) error) userMixin := schema.User{}.Mixin() userMixinFields0 := userMixin[0].Fields() _ = userMixinFields0 diff --git a/ent/schema/github_app.go b/ent/schema/github_app.go index 2186f883..194adc93 100644 --- a/ent/schema/github_app.go +++ b/ent/schema/github_app.go @@ -4,8 +4,8 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema" + "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" - "entgo.io/ent/schema/index" "github.com/unbindapp/unbind-api/ent/schema/mixin" ) @@ -17,7 +17,6 @@ type GithubApp struct { // Mixin of the GithubApp. func (GithubApp) Mixin() []ent.Mixin { return []ent.Mixin{ - mixin.PKMixin{}, mixin.TimeMixin{}, } } @@ -25,7 +24,9 @@ func (GithubApp) Mixin() []ent.Mixin { // Fields of the GithubApp. func (GithubApp) Fields() []ent.Field { return []ent.Field{ - field.Int64("github_app_id"). + field.Int64("id"). + Immutable(). + Positive(). Unique(). Comment("The GitHub App ID"), field.String("name"). @@ -47,14 +48,12 @@ func (GithubApp) Fields() []ent.Field { // Edges of the GithubApp. func (GithubApp) Edges() []ent.Edge { - return nil -} - -// Indexes of the GithubApp. -func (GithubApp) Indexes() []ent.Index { - return []ent.Index{ - index.Fields("github_app_id"). - Unique(), + return []ent.Edge{ + // O2M with github_installations + edge.To("installations", GithubInstallation.Type). + Annotations(entsql.Annotation{ + OnDelete: entsql.Cascade, + }), } } diff --git a/ent/schema/github_installation.go b/ent/schema/github_installation.go new file mode 100644 index 00000000..c7c252d0 --- /dev/null +++ b/ent/schema/github_installation.go @@ -0,0 +1,97 @@ +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect/entsql" + "entgo.io/ent/schema" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "github.com/unbindapp/unbind-api/ent/schema/mixin" + "github.com/unbindapp/unbind-api/internal/models" +) + +// GithubInstallation holds the schema definition for the GithubInstallation entity. +type GithubInstallation struct { + ent.Schema +} + +// Mixin of the GithubInstallation. +func (GithubInstallation) Mixin() []ent.Mixin { + return []ent.Mixin{ + mixin.TimeMixin{}, + } +} + +// Fields of the GithubInstallation. +func (GithubInstallation) Fields() []ent.Field { + return []ent.Field{ + field.Int64("id"). + Immutable(). + Positive(). + Unique(). + Comment("The GitHub Installation ID"), + field.Int64("github_app_id"). + Comment("The GitHub App ID this installation belongs to"), + // Account fields (common to both orgs and users) + field.Int64("account_id"). + Comment("The GitHub account ID (org or user)"), + field.String("account_login"). + NotEmpty(). + Comment("The GitHub account login (org or user name)"), + field.Enum("account_type"). + Values("Organization", "User"). + Comment("Type of GitHub account"), + field.String("account_url"). + NotEmpty(). + Comment("The HTML URL to the GitHub account"), + // Repository access + field.Enum("repository_selection"). + Values("all", "selected"). + Default("all"). + Comment("Whether the installation has access to all repos or only selected ones"), + // Status fields + field.Bool("suspended"). + Default(false). + Comment("Whether the installation is suspended"), + field.Bool("active"). + Default(true). + Comment("Whether the installation is active"), + // Permissions and settings - optional but useful + field.JSON("permissions", models.GithubInstallationPermissions{}). + Optional(). + Comment("Permissions granted to this installation"), + field.JSON("events", []string{}). + Optional(). + Comment("Events this installation subscribes to"), + } +} + +// Edges of the GithubInstallation. +func (GithubInstallation) Edges() []ent.Edge { + return []ent.Edge{ + // M2O with github_apps + edge.From("github_apps", GithubApp.Type). + Ref("installations"). + Field("github_app_id"). + Required(). + Unique(), + } +} + +// Indexes of the GithubInstallation. +func (GithubInstallation) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("github_app_id"). + Unique(), + } +} + +// Annotations of the GithubInstallation +func (GithubInstallation) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{ + Table: "github_installations", + }, + } +} diff --git a/ent/tx.go b/ent/tx.go index d2eed430..929576dc 100644 --- a/ent/tx.go +++ b/ent/tx.go @@ -16,6 +16,8 @@ type Tx struct { config // GithubApp is the client for interacting with the GithubApp builders. GithubApp *GithubAppClient + // GithubInstallation is the client for interacting with the GithubInstallation builders. + GithubInstallation *GithubInstallationClient // User is the client for interacting with the User builders. User *UserClient @@ -150,6 +152,7 @@ func (tx *Tx) Client() *Client { func (tx *Tx) init() { tx.GithubApp = NewGithubAppClient(tx.config) + tx.GithubInstallation = NewGithubInstallationClient(tx.config) tx.User = NewUserClient(tx.config) } diff --git a/internal/database/repository/github_app.go b/internal/database/repository/github_app.go index 340ec4f7..a1584d31 100644 --- a/internal/database/repository/github_app.go +++ b/internal/database/repository/github_app.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-github/v69/github" "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/ent/githubapp" ) // GetGithubApp returns the GithubApp entity., ent.NotFoundError if not found.s @@ -12,12 +13,22 @@ func (r *Repository) GetGithubApp(ctx context.Context) (*ent.GithubApp, error) { return r.DB.GithubApp.Query().First(ctx) } +// Get all github apps returns a slice of GithubApp entities. +func (r *Repository) GetGithubApps(ctx context.Context) ([]*ent.GithubApp, error) { + return r.DB.GithubApp.Query().All(ctx) +} + func (r *Repository) CreateGithubApp(ctx context.Context, app *github.AppConfig) (*ent.GithubApp, error) { return r.DB.GithubApp.Create(). - SetGithubAppID(app.GetID()). + SetID(app.GetID()). SetClientID(app.GetClientID()). SetClientSecret(app.GetClientSecret()). SetWebhookSecret(app.GetWebhookSecret()). SetPrivateKey(app.GetPEM()). + SetName(app.GetName()). Save(ctx) } + +func (r *Repository) GetGithubAppByID(ctx context.Context, ID int64) (*ent.GithubApp, error) { + return r.DB.GithubApp.Query().Where(githubapp.ID(ID)).Only(ctx) +} diff --git a/internal/database/repository/github_installation.go b/internal/database/repository/github_installation.go new file mode 100644 index 00000000..c0d19a9e --- /dev/null +++ b/internal/database/repository/github_installation.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/internal/models" +) + +func (r *Repository) GetGithubInstallationByID(ctx context.Context, ID int64) (*ent.GithubInstallation, error) { + return r.DB.GithubInstallation.Query().Where(githubinstallation.ID(ID)).Only(ctx) +} + +func (r *Repository) UpsertGithubInstallation( + ctx context.Context, + id int64, + appID int64, + accountID int64, + accountLogin string, + accountType githubinstallation.AccountType, + accountURL string, + repositorySelection githubinstallation.RepositorySelection, + suspended bool, + active bool, + permissions models.GithubInstallationPermissions, + events []string, +) (*ent.GithubInstallation, error) { + err := r.DB.GithubInstallation.Create(). + SetID(id). + SetGithubAppID(appID). + SetAccountID(accountID). + SetAccountLogin(accountLogin). + SetAccountType(accountType). + SetAccountURL(accountURL). + SetRepositorySelection(repositorySelection). + SetSuspended(suspended). + SetActive(active). + SetPermissions(permissions). + SetEvents(events). + OnConflictColumns( + githubinstallation.FieldAccountID, + githubinstallation.FieldAccountLogin, + githubinstallation.FieldAccountType, + githubinstallation.FieldAccountURL, + githubinstallation.FieldRepositorySelection, + githubinstallation.FieldSuspended, + githubinstallation.FieldActive, + githubinstallation.FieldPermissions, + githubinstallation.FieldEvents, + ). + UpdateNewValues(). + Exec(ctx) + if err != nil { + return nil, err + } + + return r.DB.GithubInstallation.Query().Where(githubinstallation.ID(id)).Only(ctx) +} + +func (r *Repository) SetInstallationActive(ctx context.Context, id int64, active bool) (*ent.GithubInstallation, error) { + return r.DB.GithubInstallation.UpdateOneID(id). + SetActive(active). + Save(ctx) +} + +func (r *Repository) SetInstallationSuspended(ctx context.Context, id int64, suspended bool) (*ent.GithubInstallation, error) { + return r.DB.GithubInstallation.UpdateOneID(id). + SetSuspended(suspended). + Save(ctx) +} diff --git a/internal/models/github_installation_metadata.go b/internal/models/github_installation_metadata.go new file mode 100644 index 00000000..c0e8c0e9 --- /dev/null +++ b/internal/models/github_installation_metadata.go @@ -0,0 +1,6 @@ +package models + +type GithubInstallationPermissions struct { + Contents string `json:"contents,omitempty"` + Metadata string `json:"metadata,omitempty"` +} diff --git a/internal/server/github.go b/internal/server/github.go index a50c8ba4..474c2584 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -3,8 +3,11 @@ package server import ( "context" "fmt" + "net/http" + "net/url" "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" "github.com/unbindapp/unbind-api/ent" "github.com/unbindapp/unbind-api/internal/github" "github.com/unbindapp/unbind-api/internal/log" @@ -78,3 +81,52 @@ func (s *Server) GithubAppConnect(ctx context.Context, input *GithubAppConnectIn resp.Body.Name = ghApp.Name return resp, nil } + +// Redirect user to install the app +type GithubAppInstallInput struct { + AppID int64 `path:"app_id" validate:"required"` +} + +type GithubAppInstallResponse struct { + Status int + Url string `header:"Location"` + Cookie string `header:"Set-Cookie"` +} + +func (s *Server) GithubAppInstall(ctx context.Context, input *GithubAppInstallInput) (*GithubAppInstallResponse, error) { + // Get the app + ghApp, err := s.Repository.GetGithubAppByID(ctx, input.AppID) + if err != nil { + if ent.IsNotFound(err) { + return nil, huma.Error404NotFound("App not found") + } + log.Error("Error getting github app", "err", err) + return nil, huma.Error500InternalServerError("Failed to get github app") + } + + // Create a state parameter to verify the callback + state := uuid.New().String() + + // create a cookie that stores the state value + cookie := &http.Cookie{ + Name: "github_install_state", + Value: state, + Path: "/", + MaxAge: int(3600), + Secure: false, + HttpOnly: true, + } + + // Redirect URL - this is where GitHub will send users to install your app + redirectURL := fmt.Sprintf( + "https://github.com/settings/apps/%s/installations/new?state=%s", + url.QueryEscape(ghApp.Name), + url.QueryEscape(state), + ) + + return &GithubAppInstallResponse{ + Status: http.StatusTemporaryRedirect, + Url: redirectURL, + Cookie: cookie.String(), + }, nil +} diff --git a/internal/server/github_webhook.go b/internal/server/github_webhook.go new file mode 100644 index 00000000..898376fb --- /dev/null +++ b/internal/server/github_webhook.go @@ -0,0 +1,149 @@ +package server + +import ( + "context" + "strings" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/go-github/v69/github" + "github.com/unbindapp/unbind-api/ent" + "github.com/unbindapp/unbind-api/ent/githubinstallation" + "github.com/unbindapp/unbind-api/internal/log" + "github.com/unbindapp/unbind-api/internal/models" +) + +// Redirect user to install the app +type GithubWebhookInput struct { + RawBody []byte `contentType:"application/json"` + Sha1SignatureHeader string `header:"X-Hub-Signature"` + Sha256SignatureHeader string `header:"X-Hub-Signature-256"` + EventType string `header:"X-GitHub-Event"` +} + +type GithubWebhookOutput struct { +} + +func (s *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookInput) (*GithubWebhookOutput, error) { + // Since we may have multiple apps, we want to validate against every webhook secret to see if it belongs to any of our apps + ghApps, err := s.Repository.GetGithubApps(ctx) + if err != nil { + log.Error("Error getting github apps", "err", err) + return nil, huma.Error500InternalServerError("Failed to get github apps") + } + var ghApp *ent.GithubApp + + // Figure out signature for webhook secret validation + signature := input.Sha256SignatureHeader + if signature == "" { + signature = input.Sha1SignatureHeader + } + + // Validate the payload using the webhook secret. + var payload []byte + for _, app := range ghApps { + err := github.ValidateSignature(signature, input.RawBody, []byte(app.WebhookSecret)) + if err == nil { + ghApp = app + break + } + } + if err != nil { + log.Error("Error validating github webhook", "err", err) + return nil, huma.Error400BadRequest("Failed to validate github webhook") + } + + // Parse the webhook event. + event, err := github.ParseWebHook(input.EventType, payload) + if err != nil { + log.Errorf("Could not parse webhook: %v", err) + return nil, huma.Error400BadRequest("Failed to parse github webhook") + } + + switch e := event.(type) { + case *github.InstallationEvent: + // Common installation data + installation := e.GetInstallation() + installationID := installation.GetID() + account := installation.GetAccount() + + // Check if this event is for our app + if installation.GetAppID() != ghApp.ID { + log.Info("Received installation event for different app", "app", e.Installation.GetAppID(), "expected", ghApp.ID) + return &GithubWebhookOutput{}, nil + } + + // Determine account type and details + accountType := githubinstallation.AccountTypeUser + if strings.EqualFold(account.GetType(), "Organization") { + accountType = githubinstallation.AccountTypeOrganization + } + + // Determine repository selection + repoSelection := githubinstallation.RepositorySelectionAll + if strings.EqualFold(installation.GetRepositorySelection(), "selected") { + repoSelection = githubinstallation.RepositorySelectionSelected + } + + // Process based on action + switch e.GetAction() { + case "created", "new_permissions_accepted": + // Build permissions map + permissions := models.GithubInstallationPermissions{} + if perms := installation.GetPermissions(); perms != nil { + permissions.Contents = perms.GetContents() + permissions.Metadata = perms.GetMetadata() + } + + // Get events + events := make([]string, 0) + for _, event := range installation.Events { + events = append(events, event) + } + + // Create or update installation in database + _, err = s.Repository.UpsertGithubInstallation( + ctx, + installationID, + installation.GetAppID(), + account.GetID(), + account.GetLogin(), + accountType, + account.GetHTMLURL(), + repoSelection, + installation.SuspendedAt != nil, + true, + permissions, + events, + ) + + if err != nil { + log.Error("Error upserting github installation", "err", err) + return nil, huma.Error500InternalServerError("Failed to upsert github installation") + } + + case "deleted": + // Mark as inactive instead of deleting + _, err := s.Repository.SetInstallationActive(ctx, installationID, false) + if err != nil { + log.Error("Error setting installation as inactive", "err", err) + return nil, huma.Error500InternalServerError("Failed to set installation as inactive") + } + + case "suspended": + _, err := s.Repository.SetInstallationSuspended(ctx, installationID, true) + if err != nil { + log.Error("Error setting installation as suspended", "err", err) + return nil, huma.Error500InternalServerError("Failed to set installation as suspended") + } + + case "unsuspended": + _, err := s.Repository.SetInstallationSuspended(ctx, installationID, false) + if err != nil { + log.Error("Error setting installation as unsuspended", "err", err) + return nil, huma.Error500InternalServerError("Failed to set installation as unsuspended") + } + } + } + + return &GithubWebhookOutput{}, nil +} From 5f85daeaf4ea96499a88544f2d152eef8dc507a2 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 3 Mar 2025 21:01:56 +0000 Subject: [PATCH 12/22] Dont lock to 1 app --- internal/server/github.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/internal/server/github.go b/internal/server/github.go index 474c2584..fc742604 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -28,17 +28,6 @@ type GithubCreateManifestResponse struct { // Create a manifest that the user can use to create a GitHub app func (s *Server) GithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { - // ! TODO - for now we only need 1 github app, so lets check uniqueness, in future we may want more for some reason - ghApp, err := s.Repository.GetGithubApp(ctx) - if ghApp != nil { - log.Info("Github app already exists") - return nil, huma.Error400BadRequest("Github app already exists") - } - if err != nil && !ent.IsNotFound(err) { - log.Error("Error getting github app", "err", err) - return nil, huma.Error500InternalServerError("Failed to get github app") - } - // Create GitHub app manifest manifest := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) From aa01a4d87fbd88530c93c9dff8d3ddf50a05c53c Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Mon, 3 Mar 2025 22:41:40 +0000 Subject: [PATCH 13/22] Add APIs to list github installations and apps --- cmd/main.go | 70 +++++++++++++++++-- internal/database/repository/github_app.go | 8 ++- .../repository/github_installation.go | 4 ++ internal/server/github.go | 60 +++++++++++++--- internal/server/github_webhook.go | 2 +- 5 files changed, 128 insertions(+), 16 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index d204561f..a5f41d27 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -91,12 +91,74 @@ func main() { ghGroup := huma.NewGroup(api, "/github") ghGroup.UseMiddleware(mw.Authenticate) - huma.Post(ghGroup, "/app/manifest", srvImpl.GithubManifestCreate) - huma.Post(ghGroup, "/app/connect", srvImpl.GithubAppConnect) - huma.Post(ghGroup, "/app/install/{app_id}", srvImpl.GithubAppInstall) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "manifest-create", + Summary: "Create Github manifest", + Description: "Create a manifest that the user can use to create a GitHub app", + Path: "/app/manifest", + Method: http.MethodPost, + }, + srvImpl.HandleGithubManifestCreate, + ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "app-connect", + Summary: "Connect github app", + Description: "Connect the new github app to our instance, via manifest code exchange", + Path: "/app/connect", + Method: http.MethodPost, + }, + srvImpl.HandleGithubAppConnect, + ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "app-install", + Summary: "App Install Redirect", + Description: "Redirects to install the github app", + Path: "/app/install/{app_id}", + Method: http.MethodPost, + }, + srvImpl.HandleGithubAppInstall, + ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "list-apps", + Summary: "List Github Apps", + Description: "List all the github apps connected to our instance", + Path: "/apps", + Method: http.MethodGet, + }, + srvImpl.HandleListGithubApps, + ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "list-app-installations", + Summary: "List Installations", + Description: "List all installations for a specific github app", + Path: "/app/{app_id}/installations", + Method: http.MethodGet, + }, + srvImpl.HandleListGithubAppInstallations, + ) webhookGroup := huma.NewGroup(api, "/webhook") - huma.Post(webhookGroup, "/github", srvImpl.HandleGithubWebhook) + huma.Register( + webhookGroup, + huma.Operation{ + OperationID: "github-webhook", + Summary: "Github Webhook", + Description: "Handle incoming github webhooks", + Path: "/github", + Method: http.MethodPost, + }, + srvImpl.HandleGithubWebhook, + ) // ! // huma.Get(api, "/teams", srvImpl.ListTeams) diff --git a/internal/database/repository/github_app.go b/internal/database/repository/github_app.go index a1584d31..763ab3da 100644 --- a/internal/database/repository/github_app.go +++ b/internal/database/repository/github_app.go @@ -14,8 +14,12 @@ func (r *Repository) GetGithubApp(ctx context.Context) (*ent.GithubApp, error) { } // Get all github apps returns a slice of GithubApp entities. -func (r *Repository) GetGithubApps(ctx context.Context) ([]*ent.GithubApp, error) { - return r.DB.GithubApp.Query().All(ctx) +func (r *Repository) GetGithubApps(ctx context.Context, withInstallations bool) ([]*ent.GithubApp, error) { + q := r.DB.GithubApp.Query() + if withInstallations { + q.WithInstallations() + } + return q.All(ctx) } func (r *Repository) CreateGithubApp(ctx context.Context, app *github.AppConfig) (*ent.GithubApp, error) { diff --git a/internal/database/repository/github_installation.go b/internal/database/repository/github_installation.go index c0d19a9e..6ba1b48f 100644 --- a/internal/database/repository/github_installation.go +++ b/internal/database/repository/github_installation.go @@ -12,6 +12,10 @@ func (r *Repository) GetGithubInstallationByID(ctx context.Context, ID int64) (* return r.DB.GithubInstallation.Query().Where(githubinstallation.ID(ID)).Only(ctx) } +func (r *Repository) GetGithubInstallationsByAppID(ctx context.Context, appID int64) ([]*ent.GithubInstallation, error) { + return r.DB.GithubInstallation.Query().Where(githubinstallation.GithubAppID(appID)).All(ctx) +} + func (r *Repository) UpsertGithubInstallation( ctx context.Context, id int64, diff --git a/internal/server/github.go b/internal/server/github.go index fc742604..a5a37030 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -27,7 +27,7 @@ type GithubCreateManifestResponse struct { } // Create a manifest that the user can use to create a GitHub app -func (s *Server) GithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { +func (s *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { // Create GitHub app manifest manifest := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) @@ -39,19 +39,19 @@ func (s *Server) GithubManifestCreate(ctx context.Context, input *GithubCreateMa } // Connect the new github app to our instance, via manifest code exchange -type GithubAppConnectInput struct { +type HandleGithubAppConnectInput struct { Body struct { Code string `json:"code"` } } -type GithubAppConnectResponse struct { +type HandleGithubAppConnectResponse struct { Body struct { Name string `json:"name"` } } -func (s *Server) GithubAppConnect(ctx context.Context, input *GithubAppConnectInput) (*GithubAppConnectResponse, error) { +func (s *Server) HandleGithubAppConnect(ctx context.Context, input *HandleGithubAppConnectInput) (*HandleGithubAppConnectResponse, error) { // Exchange the code for tokens. appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) if err != nil { @@ -66,23 +66,65 @@ func (s *Server) GithubAppConnect(ctx context.Context, input *GithubAppConnectIn } // Return the app name - resp := &GithubAppConnectResponse{} + resp := &HandleGithubAppConnectResponse{} resp.Body.Name = ghApp.Name return resp, nil } +// GET Github apps +type GithubAppListInput struct { + WithInstallations bool `query:"with_installations"` +} + +type GithubAppListResponse struct { + Body []*ent.GithubApp +} + +func (s *Server) HandleListGithubApps(ctx context.Context, input *GithubAppListInput) (*GithubAppListResponse, error) { + apps, err := s.Repository.GetGithubApps(ctx, input.WithInstallations) + if err != nil { + log.Error("Error getting github apps", "err", err) + return nil, huma.Error500InternalServerError("Failed to get github apps") + } + + resp := &GithubAppListResponse{} + resp.Body = apps + return resp, nil +} + +// GET Github app installations +type GithubAppInstallationListInput struct { + AppID int64 `path:"app_id" validate:"required"` +} + +type GithubAppInstallationListResponse struct { + Body []*ent.GithubInstallation +} + +func (s *Server) HandleListGithubAppInstallations(ctx context.Context, input *GithubAppInstallationListInput) (*GithubAppInstallationListResponse, error) { + installations, err := s.Repository.GetGithubInstallationsByAppID(ctx, input.AppID) + if err != nil { + log.Error("Error getting github installations", "err", err) + return nil, huma.Error500InternalServerError("Failed to get github installations") + } + + resp := &GithubAppInstallationListResponse{} + resp.Body = installations + return resp, nil +} + // Redirect user to install the app -type GithubAppInstallInput struct { +type HandleGithubAppInstallInput struct { AppID int64 `path:"app_id" validate:"required"` } -type GithubAppInstallResponse struct { +type HandleGithubAppInstallResponse struct { Status int Url string `header:"Location"` Cookie string `header:"Set-Cookie"` } -func (s *Server) GithubAppInstall(ctx context.Context, input *GithubAppInstallInput) (*GithubAppInstallResponse, error) { +func (s *Server) HandleGithubAppInstall(ctx context.Context, input *HandleGithubAppInstallInput) (*HandleGithubAppInstallResponse, error) { // Get the app ghApp, err := s.Repository.GetGithubAppByID(ctx, input.AppID) if err != nil { @@ -113,7 +155,7 @@ func (s *Server) GithubAppInstall(ctx context.Context, input *GithubAppInstallIn url.QueryEscape(state), ) - return &GithubAppInstallResponse{ + return &HandleGithubAppInstallResponse{ Status: http.StatusTemporaryRedirect, Url: redirectURL, Cookie: cookie.String(), diff --git a/internal/server/github_webhook.go b/internal/server/github_webhook.go index 898376fb..5f87f643 100644 --- a/internal/server/github_webhook.go +++ b/internal/server/github_webhook.go @@ -25,7 +25,7 @@ type GithubWebhookOutput struct { func (s *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookInput) (*GithubWebhookOutput, error) { // Since we may have multiple apps, we want to validate against every webhook secret to see if it belongs to any of our apps - ghApps, err := s.Repository.GetGithubApps(ctx) + ghApps, err := s.Repository.GetGithubApps(ctx, false) if err != nil { log.Error("Error getting github apps", "err", err) return nil, huma.Error500InternalServerError("Failed to get github apps") From c97af9390586f7ed59b7eed84f3719ad9886a58c Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 4 Mar 2025 00:55:03 +0000 Subject: [PATCH 14/22] Update external URL --- cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index a5f41d27..b3b79c40 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -59,7 +59,7 @@ func main() { TokenURL: cfg.DexIssuerURL + "/token", }, // ! TODO - adjust redirect when necessary - RedirectURL: "http://localhost:8089/auth/callback", + RedirectURL: fmt.Sprintf("%s/auth/callback", cfg.ExternalURL), Scopes: []string{"openid", "profile", "email", "offline_access"}, }, GithubClient: github.NewGithubClient(cfg), From 65d404f90354b45b5233902ff9257ab82dc00020 Mon Sep 17 00:00:00 2001 From: yekta Date: Tue, 4 Mar 2025 05:19:46 +0000 Subject: [PATCH 15/22] Check for bearer token specifically --- internal/middleware/auth.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index ebed8061..705488ef 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -15,6 +15,11 @@ func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { return } + if !strings.HasPrefix(authHeader, "Bearer ") { + huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") + return + } + bearerToken := strings.TrimPrefix(authHeader, "Bearer ") token, err := m.verifier.Verify(ctx.Context(), bearerToken) if err != nil { From f1eec5e3e15391a7c85a8b76d5227e03e0acd0b0 Mon Sep 17 00:00:00 2001 From: yekta Date: Tue, 4 Mar 2025 08:57:06 +0300 Subject: [PATCH 16/22] Deploy --- internal/middleware/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 705488ef..aad3535c 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -15,6 +15,7 @@ func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { return } + // if !strings.HasPrefix(authHeader, "Bearer ") { huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") return From abede99cba74a99c89788e69dc20ad1be64bb4ad Mon Sep 17 00:00:00 2001 From: yekta Date: Tue, 4 Mar 2025 08:57:13 +0300 Subject: [PATCH 17/22] Remove comment --- internal/middleware/auth.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index aad3535c..705488ef 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -15,7 +15,6 @@ func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { return } - // if !strings.HasPrefix(authHeader, "Bearer ") { huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") return From 6fcc171a988f5acbd763537a1f9ebf8297dff12e Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 4 Mar 2025 14:15:10 +0000 Subject: [PATCH 18/22] Github manifest fixes --- internal/github/manifest.go | 18 +++++++---- internal/server/github.go | 7 ++++- internal/utils/rand.go | 25 +++++++++++++++ internal/utils/rando_test.go | 59 ++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 internal/utils/rand.go create mode 100644 internal/utils/rando_test.go diff --git a/internal/github/manifest.go b/internal/github/manifest.go index 9393fe8a..1841f78e 100644 --- a/internal/github/manifest.go +++ b/internal/github/manifest.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/google/go-github/v69/github" + "github.com/unbindapp/unbind-api/internal/utils" ) // GitHubAppManifest represents the structure for GitHub App manifest @@ -22,8 +23,7 @@ type GitHubAppManifest struct { // HookAttributes contains webhook configuration type HookAttributes struct { - URL string `json:"url"` - Secret string `json:"secret"` + URL string `json:"url"` } // DefaultPermissions contains permission settings @@ -34,9 +34,15 @@ type DefaultPermissions struct { } // CreateAppManifest generates the GitHub App manifest -func (g *GithubClient) CreateAppManifest(redirectUrl string) *GitHubAppManifest { +func (g *GithubClient) CreateAppManifest(redirectUrl string) (*GitHubAppManifest, error) { + // Generate a random suffix + suffixRand, err := utils.GenerateRandomSimpleID(5) + if err != nil { + return nil, fmt.Errorf("failed to generate random suffix: %w", err) + } + return &GitHubAppManifest{ - Name: fmt.Sprintf("unbind-%s", g.cfg.UnbindSuffix), + Name: fmt.Sprintf("unbind-%s-%s", g.cfg.UnbindSuffix, suffixRand), Description: "Application to connect unbind with Github", URL: g.cfg.ExternalURL, HookAttributes: HookAttributes{ @@ -49,8 +55,8 @@ func (g *GithubClient) CreateAppManifest(redirectUrl string) *GitHubAppManifest Issues: "write", Metadata: "read", }, - DefaultEvents: []string{"push", "pull_request"}, - } + DefaultEvents: []string{"push"}, + }, nil } // ManifestCodeConversion gets app configruation from github using the code diff --git a/internal/server/github.go b/internal/server/github.go index a5a37030..6285a692 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -29,7 +29,12 @@ type GithubCreateManifestResponse struct { // Create a manifest that the user can use to create a GitHub app func (s *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { // Create GitHub app manifest - manifest := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) + manifest, err := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) + + if err != nil { + log.Error("Error creating github app manifest", "err", err) + return nil, huma.Error500InternalServerError("Failed to create github app manifest") + } // Create resp resp := &GithubCreateManifestResponse{} diff --git a/internal/utils/rand.go b/internal/utils/rand.go new file mode 100644 index 00000000..aa3a6e2a --- /dev/null +++ b/internal/utils/rand.go @@ -0,0 +1,25 @@ +package utils + +import ( + "crypto/rand" + "math/big" +) + +func GenerateRandomSimpleID(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + charsetLength := big.NewInt(int64(len(charset))) + + result := make([]byte, length) + + // Fill the slice with random characters from the charset + for i := 0; i < length; i++ { + randomIndex, err := rand.Int(rand.Reader, charsetLength) + if err != nil { + return "", err + } + + result[i] = charset[randomIndex.Int64()] + } + + return string(result), nil +} diff --git a/internal/utils/rando_test.go b/internal/utils/rando_test.go new file mode 100644 index 00000000..0dc45ee5 --- /dev/null +++ b/internal/utils/rando_test.go @@ -0,0 +1,59 @@ +package utils + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateRandomSimpleID(t *testing.T) { + t.Run("generates string of correct length", func(t *testing.T) { + length := 5 + result, err := GenerateRandomSimpleID(length) + + require.NoError(t, err) + assert.Equal(t, length, len(result), "Generated string should be of the requested length") + }) + + t.Run("generates only lowercase alphanumeric characters", func(t *testing.T) { + result, err := GenerateRandomSimpleID(5) + + require.NoError(t, err) + matched, err := regexp.MatchString("^[a-z0-9]+$", result) + require.NoError(t, err, "Regex matching error") + assert.True(t, matched, "String should only contain lowercase letters and numbers") + }) + + t.Run("generates different strings on consecutive calls", func(t *testing.T) { + // Generate multiple strings and ensure they're different + results := make(map[string]bool) + + // Generate 100 strings to have a statistically significant sample + for i := 0; i < 100; i++ { + str, err := GenerateRandomSimpleID(5) + require.NoError(t, err) + results[str] = true + } + + // If truly random, we should have close to 100 unique strings + // We use a lower bound to account for possible collisions + assert.Greater(t, len(results), 95, "Should generate mostly unique strings") + }) + + t.Run("handles zero length", func(t *testing.T) { + result, err := GenerateRandomSimpleID(0) + + require.NoError(t, err) + assert.Equal(t, "", result, "Zero length should return empty string") + }) + + t.Run("handles large length", func(t *testing.T) { + length := 1000 + result, err := GenerateRandomSimpleID(length) + + require.NoError(t, err) + assert.Equal(t, length, len(result), "Should handle large requested lengths") + }) +} From 38ac2a464f8fcb09966f87e7beaf0d43a6d314ce Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Tue, 4 Mar 2025 20:10:51 +0000 Subject: [PATCH 19/22] Re-add pull request --- internal/github/manifest.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/github/manifest.go b/internal/github/manifest.go index 1841f78e..51dca09e 100644 --- a/internal/github/manifest.go +++ b/internal/github/manifest.go @@ -28,9 +28,10 @@ type HookAttributes struct { // DefaultPermissions contains permission settings type DefaultPermissions struct { - Contents string `json:"contents"` - Issues string `json:"issues"` - Metadata string `json:"metadata"` + Contents string `json:"contents"` + Issues string `json:"issues"` + Metadata string `json:"metadata"` + PullRequests string `json:"pull_requests"` } // CreateAppManifest generates the GitHub App manifest @@ -51,11 +52,12 @@ func (g *GithubClient) CreateAppManifest(redirectUrl string) (*GitHubAppManifest RedirectURL: redirectUrl, Public: false, DefaultPermissions: DefaultPermissions{ - Contents: "read", - Issues: "write", - Metadata: "read", + Contents: "read", + Issues: "write", + Metadata: "read", + PullRequests: "read", }, - DefaultEvents: []string{"push"}, + DefaultEvents: []string{"push", "pull_request"}, }, nil } From 54b084446e2c112adb8fee5e7ebd093445ef7978 Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Wed, 5 Mar 2025 14:30:07 +0000 Subject: [PATCH 20/22] Add github app create/save API --- cmd/main.go | 22 +++++ internal/server/github_apps.go | 169 +++++++++++++++++++++++++++++++++ internal/utils/url.go | 11 +++ internal/utils/url_test.go | 96 +++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 internal/server/github_apps.go diff --git a/cmd/main.go b/cmd/main.go index b3b79c40..47b90ec3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -91,6 +91,28 @@ func main() { ghGroup := huma.NewGroup(api, "/github") ghGroup.UseMiddleware(mw.Authenticate) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "app-create", + Summary: "Create Github app", + Description: "Begin the workflow to create a github application", + Path: "/app/create", + Method: http.MethodGet, + }, + srvImpl.HandleGithubAppCreate, + ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "app-save", + Summary: "Save github app", + Description: "Save github app via code exchange and redirect to installation", + Path: "/app/save", + Method: http.MethodGet, + }, + srvImpl.HandleGithubAppSave, + ) huma.Register( ghGroup, huma.Operation{ diff --git a/internal/server/github_apps.go b/internal/server/github_apps.go new file mode 100644 index 00000000..3d9cfad2 --- /dev/null +++ b/internal/server/github_apps.go @@ -0,0 +1,169 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "html/template" + "net/http" + "net/url" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/unbindapp/unbind-api/internal/log" + "github.com/unbindapp/unbind-api/internal/utils" +) + +type GithubAppCreateInput struct { + Body struct { + } +} + +type GithubAppCreateResponse struct { + ContentType string `header:"Content-Type"` + Body []byte +} + +// Handler to render GitHub page with form submission +func (s *Server) HandleGithubAppCreate(ctx context.Context, input *EmptyInput) (*GithubAppCreateResponse, error) { + // Template for the GitHub form submission page + tmpl := ` + + + GitHub Form Submit + + + + +` + + // Build redirect + redirect, err := utils.JoinURLPaths(s.Cfg.ExternalURL, "/github/app/save") + if err != nil { + log.Error("Error building redirect URL", "err", err) + return nil, huma.Error500InternalServerError("Failed to build redirect URL") + } + + // Create GitHub app manifest + manifest, err := s.GithubClient.CreateAppManifest(redirect) + githubUrl := fmt.Sprintf("%s/settings/apps/new", s.Cfg.GithubURL) + + if err != nil { + log.Error("Error creating github app manifest", "err", err) + return nil, huma.Error500InternalServerError("Failed to create github app manifest") + } + + // Create template data struct + type templateData struct { + PostURL string + ManifestJSON template.JS + } + + // Convert manifest to JSON + manifestJSON, err := json.Marshal(manifest) + if err != nil { + log.Error("Error marshaling manifest to JSON", "err", err) + return nil, huma.Error500InternalServerError("Failed to prepare manifest data") + } + + // Create data for template + data := templateData{ + PostURL: githubUrl, + ManifestJSON: template.JS(string(manifestJSON)), + } + + // Parse and execute the template + t, err := template.New("github-form").Parse(tmpl) + if err != nil { + log.Error("Error parsing template", "err", err) + return nil, huma.Error500InternalServerError("Failed to parse HTML template") + } + + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + log.Error("Error executing template", "err", err) + return nil, huma.Error500InternalServerError("Failed to render HTML template") + } + + // Create response + resp := &GithubAppCreateResponse{ + ContentType: "text/html; charset=utf-8", + Body: buf.Bytes(), + } + + return resp, nil +} + +// Connect the new github app to our instance, via manifest code exchange +type HandleGithubAppSaveInput struct { + Body struct { + Code string `json:"code"` + } +} + +type HandleGithubAppSaveResponse struct { + Status int + Url string `header:"Location"` + Cookie string `header:"Set-Cookie"` +} + +// Save github app and redirect to installation +func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubAppSaveInput) (*HandleGithubAppSaveResponse, error) { + // Exchange the code for tokens. + appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) + if err != nil { + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange manifest code: %v", err)) + } + + // Save the app config + ghApp, err := s.Repository.CreateGithubApp(ctx, appConfig) + if err != nil { + log.Error("Error saving github app", "err", err) + return nil, huma.Error500InternalServerError("Failed to save github app") + } + + // Create a state parameter to verify the callback + state := uuid.New().String() + + // create a cookie that stores the state value + cookie := &http.Cookie{ + Name: "github_install_state", + Value: state, + Path: "/", + MaxAge: int(3600), + Secure: false, + HttpOnly: true, + } + + // Redirect URL - this is where GitHub will send users to install your app + redirectURL := fmt.Sprintf( + "https://github.com/settings/apps/%s/installations/new?state=%s", + url.QueryEscape(ghApp.Name), + url.QueryEscape(state), + ) + + return &HandleGithubAppSaveResponse{ + Status: http.StatusTemporaryRedirect, + Url: redirectURL, + Cookie: cookie.String(), + }, nil +} diff --git a/internal/utils/url.go b/internal/utils/url.go index 254ac813..f6e99500 100644 --- a/internal/utils/url.go +++ b/internal/utils/url.go @@ -3,10 +3,21 @@ package utils import ( "fmt" "net/url" + "path" "regexp" "strings" ) +func JoinURLPaths(baseURL string, paths ...string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", err + } + + u.Path = path.Join(append([]string{u.Path}, paths...)...) + return u.String(), nil +} + // ValidateAndExtractDomain takes a URL string and: // 1. Validates if it's a proper URL (must start with http:// or https://) // 2. Extracts the domain name diff --git a/internal/utils/url_test.go b/internal/utils/url_test.go index 1a82870f..c94ec976 100644 --- a/internal/utils/url_test.go +++ b/internal/utils/url_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTransformDomain(t *testing.T) { @@ -192,3 +193,98 @@ func TestValidateAndExtractDomain(t *testing.T) { }) } } + +func TestJoinURLPaths(t *testing.T) { + tests := []struct { + name string + baseURL string + paths []string + expected string + wantErr bool + }{ + { + name: "simple join", + baseURL: "https://google.com", + paths: []string{"search", "fiveguys"}, + expected: "https://google.com/search/fiveguys", + wantErr: false, + }, + { + name: "base URL with trailing slash", + baseURL: "https://google.com/", + paths: []string{"search", "fiveguys"}, + expected: "https://google.com/search/fiveguys", + wantErr: false, + }, + { + name: "paths with leading slashes", + baseURL: "https://google.com", + paths: []string{"/search", "/fiveguys"}, + expected: "https://google.com/search/fiveguys", + wantErr: false, + }, + { + name: "paths with trailing slashes", + baseURL: "https://google.com", + paths: []string{"search/", "fiveguys/"}, + expected: "https://google.com/search/fiveguys", + wantErr: false, + }, + { + name: "empty paths", + baseURL: "https://google.com", + paths: []string{}, + expected: "https://google.com", + wantErr: false, + }, + { + name: "with query parameters", + baseURL: "https://google.com?param=value", + paths: []string{"search", "fiveguys"}, + expected: "https://google.com/search/fiveguys?param=value", + wantErr: false, + }, + { + name: "with fragment", + baseURL: "https://google.com#fragment", + paths: []string{"search", "fiveguys"}, + expected: "https://google.com/search/fiveguys#fragment", + wantErr: false, + }, + { + name: "with existing path", + baseURL: "https://google.com/api", + paths: []string{"search", "fiveguys"}, + expected: "https://google.com/api/search/fiveguys", + wantErr: false, + }, + { + name: "with double slashes", + baseURL: "https://google.com", + paths: []string{"search//", "//fiveguys"}, + expected: "https://google.com/search/fiveguys", + wantErr: false, + }, + { + name: "invalid URL", + baseURL: ":invalid-url", + paths: []string{"search", "fiveguys"}, + expected: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := JoinURLPaths(tt.baseURL, tt.paths...) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} From ebf8fe7b6f0cb431347675b0d48923d9ce682f2c Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Thu, 6 Mar 2025 14:24:17 +0000 Subject: [PATCH 21/22] Github APIs for redirecting and installing app --- .devcontainer/docker-compose.yaml | 7 + cmd/main.go | 33 +- config/config.go | 2 + go.mod | 9 +- go.sum | 20 +- internal/cache/cache.go | 170 ++++++ internal/cache/cache_test.go | 357 +++++++++++++ internal/database/valkey_cache.go | 294 +++++++++++ internal/database/valkey_cache_test.go | 497 ++++++++++++++++++ internal/github/manifest.go | 10 +- internal/middleware/auth.go | 26 +- internal/server/github.go | 2 +- internal/server/github_apps.go | 44 +- internal/server/server.go | 2 + .../utils/{rando_test.go => rand_test.go} | 0 15 files changed, 1416 insertions(+), 57 deletions(-) create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/cache_test.go create mode 100644 internal/database/valkey_cache.go create mode 100644 internal/database/valkey_cache_test.go rename internal/utils/{rando_test.go => rand_test.go} (100%) diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index 1ceca01f..189f5280 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -17,6 +17,12 @@ services: networks: - app-network + valkey: + container_name: unbind_valkey + image: valkey/valkey:8 + restart: unless-stopped + networks: ['app-network'] + dex: container_name: unbind_dex image: ghcr.io/dexidp/dex:v2.41.1 @@ -48,6 +54,7 @@ services: - DEX_ISSUER_URL_EXTERNAL=http://localhost:5556 - DEX_CLIENT_ID=unbind-dev - DEX_CLIENT_SECRET=supersecret + - VALKEY_URL=valkey:6379 ports: - '127.0.0.1:8089:8089' volumes: diff --git a/cmd/main.go b/cmd/main.go index 47b90ec3..f0247bf7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,15 +17,23 @@ import ( "github.com/unbindapp/unbind-api/internal/log" "github.com/unbindapp/unbind-api/internal/middleware" "github.com/unbindapp/unbind-api/internal/server" + "github.com/valkey-io/valkey-go" "golang.org/x/oauth2" ) func main() { - godotenv.Load() + err := godotenv.Overload() // Initialize config cfg := config.NewConfig() + // Initialize valkey (redis) + client, err := valkey.NewClient(valkey.ClientOption{InitAddress: []string{cfg.ValkeyURL}}) + if err != nil { + panic(err) + } + defer client.Close() + // Load database dbConnInfo, err := database.GetSqlDbConn(cfg, false) if err != nil { @@ -63,6 +71,7 @@ func main() { Scopes: []string{"openid", "profile", "email", "offline_access"}, }, GithubClient: github.NewGithubClient(cfg), + StringCache: database.NewStringCache(client, "unbind"), } // New chi router @@ -102,17 +111,6 @@ func main() { }, srvImpl.HandleGithubAppCreate, ) - huma.Register( - ghGroup, - huma.Operation{ - OperationID: "app-save", - Summary: "Save github app", - Description: "Save github app via code exchange and redirect to installation", - Path: "/app/save", - Method: http.MethodGet, - }, - srvImpl.HandleGithubAppSave, - ) huma.Register( ghGroup, huma.Operation{ @@ -181,6 +179,17 @@ func main() { }, srvImpl.HandleGithubWebhook, ) + huma.Register( + ghGroup, + huma.Operation{ + OperationID: "app-save", + Summary: "Save github app", + Description: "Save github app via code exchange and redirect to installation", + Path: "/github/app/save", + Method: http.MethodGet, + }, + srvImpl.HandleGithubAppSave, + ) // ! // huma.Get(api, "/teams", srvImpl.ListTeams) diff --git a/config/config.go b/config/config.go index 85c436a3..2d069be5 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,8 @@ type Config struct { PostgresUser string `env:"POSTGRES_USER" envDefault:"postgres"` PostgresPassword string `env:"POSTGRES_PASSWORD" envDefault:"postgres"` PostgresDB string `env:"POSTGRES_DB" envDefault:"unbind"` + // Valkey (redis) + ValkeyURL string `env:"VALKEY_URL" envDefault:"localhost:6379"` // Dex (OIDC provider) DexIssuerURL string `env:"DEX_ISSUER_URL"` DexIssuerUrlExternal string `env:"DEX_ISSUER_URL_EXTERNAL"` diff --git a/go.mod b/go.mod index d3a0d16a..69ef1fd5 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,15 @@ require ( github.com/coreos/go-oidc/v3 v3.12.0 github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb github.com/go-chi/chi/v5 v5.2.1 + github.com/google/go-github/v69 v69.2.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.2 github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.9.0 + github.com/valkey-io/valkey-go v1.0.55 + github.com/valkey-io/valkey-go/mock v1.0.55 + go.uber.org/mock v0.5.0 + golang.org/x/oauth2 v0.23.0 k8s.io/apimachinery v0.32.2 k8s.io/client-go v0.32.2 modernc.org/sqlite v1.35.0 @@ -36,8 +41,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/go-github/v69 v69.2.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/hcl/v2 v2.13.0 // indirect @@ -69,7 +73,6 @@ require ( golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.35.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.29.0 // indirect diff --git a/go.sum b/go.sum index ec687b61..f1b1c303 100644 --- a/go.sum +++ b/go.sum @@ -24,10 +24,6 @@ github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41k github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/danielgtaylor/huma/v2 v2.28.0 h1:W+hIT52MigO73edJNJWXU896uC99xSBWpKoE2PRyybM= -github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wXiMIu94LV/cH2Ek= -github.com/danielgtaylor/huma/v2 v2.29.0 h1:MPtwpWe6WWkklai1zpbapCxvwV2J2V2Tc3N2nNuUat0= -github.com/danielgtaylor/huma/v2 v2.29.0/go.mod h1:9BxJwkeoPPDEJ2Bg4yPwL1mM1rYpAwCAWFKoo723spk= github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb h1:i2LxO2SfvVq9d6Cejj+A8dPFyWg9VzE/xJp3IlKSPuU= github.com/danielgtaylor/huma/v2 v2.29.1-0.20250224183453-44149b0847bb/go.mod h1:9BxJwkeoPPDEJ2Bg4yPwL1mM1rYpAwCAWFKoo723spk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -66,8 +62,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -128,8 +124,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -144,8 +140,6 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -155,6 +149,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valkey-io/valkey-go v1.0.55 h1:mvsiXNwHO9YrkBPzumrnFNhDAmVkZxyQsiAm6Y4c/Bg= +github.com/valkey-io/valkey-go v1.0.55/go.mod h1:yYgsDepzuxY1NjAzpmt5QV6BLCvRXyJ/M27NuaznGd4= +github.com/valkey-io/valkey-go/mock v1.0.55 h1:Acg16QLFwRcGtCWon7RPFZ+U1NiDZoWwO1yzBj2/7Xg= +github.com/valkey-io/valkey-go/mock v1.0.55/go.mod h1:ii9JXh/bnNz9tiiL7QhlEq+Zm4O8wBR2mxwHSYODAiE= github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -165,6 +163,8 @@ github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8 github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 00000000..da719958 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,170 @@ +package cache + +import ( + "sync" + "time" +) + +// An item in the cache +type Item[T any] struct { + Value T + Expiration int64 // Unix timestamp for expiration + Created int64 // Unix timestamp for creation time +} + +func (item Item[T]) Expired() bool { + if item.Expiration == 0 { + return false + } + return time.Now().UnixNano() > item.Expiration +} + +// Cache is an in-memory key:value store +type Cache[T any] struct { + items map[string]Item[T] + mu sync.RWMutex +} + +// Generic type alias for a map of items +type Items[T any] = map[string]Item[T] + +func NewCache[T any]() *Cache[T] { + return &Cache[T]{ + items: make(map[string]Item[T]), + } +} + +// Set adds an item to the cache with the default expiration time +func (self *Cache[T]) Set(key string, value T) { + self.mu.Lock() + self.items[key] = Item[T]{ + Value: value, + Expiration: 0, + Created: time.Now().UnixNano(), + } + self.mu.Unlock() +} + +// SetWithExpiration adds an item to the cache with a custom expiration time +func (self *Cache[T]) SetWithExpiration(key string, value T, duration time.Duration) { + var expiration int64 + + if duration <= 0 { + // No expiry + expiration = 0 + } else if duration > 0 { + expiration = time.Now().Add(duration).UnixNano() + } + + self.mu.Lock() + self.items[key] = Item[T]{ + Value: value, + Expiration: expiration, + Created: time.Now().UnixNano(), + } + self.mu.Unlock() +} + +// Get retrieves an item from the cache +// Returns the item and a bool indicating if the item was found +func (self *Cache[T]) Get(key string) (T, bool) { + self.mu.RLock() + item, found := self.items[key] + self.mu.RUnlock() + + if !found { + var zero T + return zero, false + } + + // If the item has expired, delete it and return not found + if item.Expired() { + self.Delete(key) + var zero T + return zero, false + } + + return item.Value, true +} + +// GetItem retrieves the entire cache item including metadata +func (self *Cache[T]) GetItem(key string) (Item[T], bool) { + self.mu.RLock() + item, found := self.items[key] + self.mu.RUnlock() + + if !found { + return Item[T]{}, false + } + + if item.Expired() { + self.Delete(key) + return Item[T]{}, false + } + + return item, true +} + +// Delete removes an item from the cache +func (self *Cache[T]) Delete(key string) { + self.mu.Lock() + delete(self.items, key) + self.mu.Unlock() +} + +// Clear empties the entire cache +func (self *Cache[T]) Clear() { + self.mu.Lock() + self.items = make(map[string]Item[T]) + self.mu.Unlock() +} + +// Count returns the number of items in the cache (including expired items) +func (self *Cache[T]) Count() int { + self.mu.Lock() + defer self.mu.Unlock() + + count := 0 + now := time.Now().UnixNano() + + toDelete := make([]string, 0) + for key, item := range self.items { + if item.Expiration == 0 || now < item.Expiration { + count++ + continue + } + // Mark for deletion + toDelete = append(toDelete, key) + } + // Delete items + for _, key := range toDelete { + delete(self.items, key) + } + + return count +} + +// ItemsNotExpired returns all non-expired items in the cache +func (self *Cache[T]) Items() Items[T] { + self.mu.Lock() + defer self.mu.Unlock() + + items := make(Items[T]) + now := time.Now().UnixNano() + + toDelete := make([]string, 0) + for k, v := range self.items { + if v.Expiration == 0 || now < v.Expiration { + items[k] = v + continue + } + // Mark for deletion + toDelete = append(toDelete, k) + } + // Delete items + for _, key := range toDelete { + delete(self.items, key) + } + + return items +} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go new file mode 100644 index 00000000..33d353d0 --- /dev/null +++ b/internal/cache/cache_test.go @@ -0,0 +1,357 @@ +package cache + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +// CacheSuite is a test suite for the Cache implementation +type CacheSuite struct { + suite.Suite + stringCache *Cache[string] + intCache *Cache[int] + userCache *Cache[User] +} + +// User is a test struct for cache storage +type User struct { + ID int + Name string +} + +// SetupTest runs before each test +func (suite *CacheSuite) SetupTest() { + suite.stringCache = NewCache[string]() + suite.intCache = NewCache[int]() + suite.userCache = NewCache[User]() +} + +// TestSet tests the Set method +func (suite *CacheSuite) TestSet() { + // String cache + suite.stringCache.Set("key1", "value1") + value, found := suite.stringCache.Get("key1") + suite.True(found, "Item should be found") + suite.Equal("value1", value, "Value should match") + + // Int cache + suite.intCache.Set("num", 42) + num, found := suite.intCache.Get("num") + suite.True(found, "Item should be found") + suite.Equal(42, num, "Value should match") + + // Struct cache + user := User{ID: 1, Name: "John"} + suite.userCache.Set("user1", user) + retrievedUser, found := suite.userCache.Get("user1") + suite.True(found, "Item should be found") + suite.Equal(user, retrievedUser, "Value should match") +} + +// TestSetWithExpiration tests the SetWithExpiration method +func (suite *CacheSuite) TestSetWithExpiration() { + // Test with positive duration (should expire) + suite.stringCache.SetWithExpiration("temp", "I'll expire", 10*time.Millisecond) + + // Verify it exists initially + value, found := suite.stringCache.Get("temp") + suite.True(found, "Item should be found before expiration") + suite.Equal("I'll expire", value, "Value should match") + + // Wait for expiration + time.Sleep(20 * time.Millisecond) + + // Verify it's gone after expiration + _, found = suite.stringCache.Get("temp") + suite.False(found, "Item should not be found after expiration") + + // Test with zero duration (should never expire) + suite.stringCache.SetWithExpiration("forever", "I won't expire", 0) + + // Wait a bit + time.Sleep(20 * time.Millisecond) + + // Verify it still exists + value, found = suite.stringCache.Get("forever") + suite.True(found, "Item with zero expiration should not expire") + suite.Equal("I won't expire", value, "Value should match") + + // Test with negative duration (should never expire) + suite.stringCache.SetWithExpiration("also-forever", "I won't expire either", -1*time.Second) + + // Wait a bit + time.Sleep(20 * time.Millisecond) + + // Verify it still exists + value, found = suite.stringCache.Get("also-forever") + suite.True(found, "Item with negative expiration should not expire") + suite.Equal("I won't expire either", value, "Value should match") +} + +// TestGetItem tests the GetItem method +func (suite *CacheSuite) TestGetItem() { + now := time.Now().UnixNano() + + suite.stringCache.Set("key1", "value1") + + item, found := suite.stringCache.GetItem("key1") + suite.True(found, "Item should be found") + suite.Equal("value1", item.Value, "Value should match") + suite.Equal(int64(0), item.Expiration, "No expiration should be 0") + suite.True(item.Created >= now, "Created timestamp should be after test start") + + // Test with expiration + suite.stringCache.SetWithExpiration("temp", "expiring", 100*time.Millisecond) + + item, found = suite.stringCache.GetItem("temp") + suite.True(found, "Item should be found") + suite.Equal("expiring", item.Value, "Value should match") + suite.True(item.Expiration > now, "Expiration should be in the future") + + // Test expired item + suite.stringCache.SetWithExpiration("expired", "gone", 10*time.Millisecond) + time.Sleep(20 * time.Millisecond) + + _, found = suite.stringCache.GetItem("expired") + suite.False(found, "Expired item should not be found") +} + +// TestDelete tests the Delete method +func (suite *CacheSuite) TestDelete() { + // Add an item + suite.stringCache.Set("key1", "value1") + + // Verify it exists + _, found := suite.stringCache.Get("key1") + suite.True(found, "Item should be found before deletion") + + // Delete it + suite.stringCache.Delete("key1") + + // Verify it's gone + _, found = suite.stringCache.Get("key1") + suite.False(found, "Item should not be found after deletion") + + // Delete non-existent key (should not panic) + suite.NotPanics(func() { + suite.stringCache.Delete("non-existent") + }, "Deleting non-existent key should not panic") +} + +// TestClear tests the Clear method +func (suite *CacheSuite) TestClear() { + // Add some items + suite.stringCache.Set("key1", "value1") + suite.stringCache.Set("key2", "value2") + + // Verify they exist + count := suite.stringCache.Count() + suite.Equal(2, count, "Cache should have 2 items before clearing") + + // Clear the cache + suite.stringCache.Clear() + + // Verify it's empty + count = suite.stringCache.Count() + suite.Equal(0, count, "Cache should be empty after clearing") +} + +// TestCount tests the Count method +func (suite *CacheSuite) TestCount() { + // Empty cache + count := suite.stringCache.Count() + suite.Equal(0, count, "Empty cache should have 0 items") + + // Add some items + suite.stringCache.Set("key1", "value1") + suite.stringCache.Set("key2", "value2") + + count = suite.stringCache.Count() + suite.Equal(2, count, "Cache should have 2 items") + + // Add an item that expires quickly + suite.stringCache.SetWithExpiration("temp", "expiring", 10*time.Millisecond) + + count = suite.stringCache.Count() + suite.Equal(3, count, "Cache should have 3 items before expiration") + + // Wait for expiration + time.Sleep(20 * time.Millisecond) + + // Count should automatically clean up expired items + count = suite.stringCache.Count() + suite.Equal(2, count, "Count should remove expired items") +} + +// TestItems tests the Items method +func (suite *CacheSuite) TestItems() { + // Add some items + suite.stringCache.Set("key1", "value1") + suite.stringCache.Set("key2", "value2") + suite.stringCache.SetWithExpiration("temp", "expiring", 10*time.Millisecond) + + // Get all items + items := suite.stringCache.Items() + suite.Equal(3, len(items), "Items should return all 3 items before expiration") + + // Verify item values + suite.Equal("value1", items["key1"].Value, "Item value should match") + suite.Equal("value2", items["key2"].Value, "Item value should match") + suite.Equal("expiring", items["temp"].Value, "Item value should match") + + // Wait for expiration + time.Sleep(20 * time.Millisecond) + + // Get items again - should clean up expired + items = suite.stringCache.Items() + suite.Equal(2, len(items), "Items should clean up expired items") + suite.Contains(items, "key1", "Items should contain permanent keys") + suite.Contains(items, "key2", "Items should contain permanent keys") + suite.NotContains(items, "temp", "Items should not contain expired keys") +} + +// TestConcurrentAccess tests concurrent access to the cache +func (suite *CacheSuite) TestConcurrentAccess() { + const goroutines = 10 + const operationsPerGoroutine = 100 + + done := make(chan bool, goroutines*2) + + // Start writers + for i := 0; i < goroutines; i++ { + go func(id int) { + for j := 0; j < operationsPerGoroutine; j++ { + key := fmt.Sprintf("key-%d-%d", id, j) + suite.stringCache.Set(key, fmt.Sprintf("value-%d-%d", id, j)) + } + done <- true + }(i) + } + + // Start readers + for i := 0; i < goroutines; i++ { + go func(id int) { + for j := 0; j < operationsPerGoroutine; j++ { + // Mix of operations: get, count, items + switch j % 3 { + case 0: + key := fmt.Sprintf("key-%d-%d", id, j) + suite.stringCache.Get(key) + case 1: + suite.stringCache.Count() + case 2: + suite.stringCache.Items() + } + } + done <- true + }(i) + } + + // Wait for all goroutines to finish + for i := 0; i < goroutines*2; i++ { + <-done + } + + // If we got here without deadlock or panic, the test passes +} + +// TestZeroValueHandling tests that zero values are handled correctly +func (suite *CacheSuite) TestZeroValueHandling() { + // Initialize with zero values + suite.intCache.Set("zero", 0) + suite.stringCache.Set("empty", "") + suite.userCache.Set("nobody", User{}) + + // Test getting the zero values + zeroInt, found := suite.intCache.Get("zero") + suite.True(found, "Zero int should be found") + suite.Equal(0, zeroInt, "Zero int value should be 0") + + emptyString, found := suite.stringCache.Get("empty") + suite.True(found, "Empty string should be found") + suite.Equal("", emptyString, "Empty string value should be empty") + + emptyUser, found := suite.userCache.Get("nobody") + suite.True(found, "Empty user should be found") + suite.Equal(User{}, emptyUser, "Empty user should equal the zero value for User") + + // Test missing values + missingInt, found := suite.intCache.Get("missing") + suite.False(found, "Missing key should not be found") + suite.Equal(0, missingInt, "Missing int should return zero value (0)") + + missingString, found := suite.stringCache.Get("missing") + suite.False(found, "Missing key should not be found") + suite.Equal("", missingString, "Missing string should return zero value (empty string)") + + missingUser, found := suite.userCache.Get("missing") + suite.False(found, "Missing key should not be found") + suite.Equal(User{}, missingUser, "Missing user should return zero value for User") +} + +// TestItemExpired tests the Expired method of Item directly +func (suite *CacheSuite) TestItemExpired() { + now := time.Now() + + tests := []struct { + name string + expiration int64 + expected bool + }{ + { + name: "zero expiration", + expiration: 0, + expected: false, + }, + { + name: "future expiration", + expiration: now.Add(1 * time.Minute).UnixNano(), + expected: false, + }, + { + name: "past expiration", + expiration: now.Add(-1 * time.Minute).UnixNano(), + expected: true, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + item := Item[string]{ + Value: "test", + Expiration: tc.expiration, + Created: now.UnixNano(), + } + + suite.Equal(tc.expected, item.Expired(), "Expired() should return expected value") + }) + } +} + +// TestNewCache tests the NewCache constructor +func (suite *CacheSuite) TestNewCache() { + // Test with different types + stringCache := NewCache[string]() + suite.NotNil(stringCache, "NewCache should return a non-nil cache") + suite.NotNil(stringCache.items, "Cache items map should be initialized") + + intCache := NewCache[int]() + suite.NotNil(intCache, "NewCache should return a non-nil cache") + + // Custom type + type Person struct { + Name string + Age int + } + + personCache := NewCache[Person]() + suite.NotNil(personCache, "NewCache should return a non-nil cache") +} + +// Run the test suite +func TestCacheSuite(t *testing.T) { + suite.Run(t, new(CacheSuite)) +} diff --git a/internal/database/valkey_cache.go b/internal/database/valkey_cache.go new file mode 100644 index 00000000..80b97283 --- /dev/null +++ b/internal/database/valkey_cache.go @@ -0,0 +1,294 @@ +package database + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/valkey-io/valkey-go" +) + +// ValueCoder handles encoding and decoding of cache values. +// This allows for generic type support with Valkey. +type ValueCoder[T any] interface { + Encode(T) (string, error) + Decode(string) (T, error) +} + +// JSONValueCoder implements ValueCoder using JSON encoding/decoding. +type JSONValueCoder[T any] struct{} + +// Encode converts a value to a JSON string. +func (c JSONValueCoder[T]) Encode(value T) (string, error) { + data, err := json.Marshal(value) + if err != nil { + return "", fmt.Errorf("failed to JSON encode: %w", err) + } + return string(data), nil +} + +// Decode converts a JSON string back to a value. +func (c JSONValueCoder[T]) Decode(data string) (T, error) { + var value T + if err := json.Unmarshal([]byte(data), &value); err != nil { + return value, fmt.Errorf("failed to JSON decode: %w", err) + } + return value, nil +} + +// StringValueCoder is a specialized coder for string values that doesn't need JSON. +type StringValueCoder struct{} + +// Encode simply returns the string as is. +func (c StringValueCoder) Encode(value string) (string, error) { + return value, nil +} + +// Decode simply returns the string as is. +func (c StringValueCoder) Decode(data string) (string, error) { + return data, nil +} + +// ValkeyCache is a wrapper around a Valkey client that adds generic type support. +type ValkeyCache[T any] struct { + client valkey.Client + keyPrefix string + coder ValueCoder[T] +} + +// NewCache creates a new cache with the specified Valkey client. +func NewCache[T any](client valkey.Client, keyPrefix string) *ValkeyCache[T] { + return &ValkeyCache[T]{ + client: client, + keyPrefix: keyPrefix, + coder: JSONValueCoder[T]{}, + } +} + +// NewStringCache creates a specialized cache for string values that avoids JSON overhead. +func NewStringCache(client valkey.Client, keyPrefix string) *ValkeyCache[string] { + return &ValkeyCache[string]{ + client: client, + keyPrefix: keyPrefix, + coder: StringValueCoder{}, + } +} + +// WithCoder allows customizing the value encoding/decoding mechanism. +func (self *ValkeyCache[T]) WithCoder(coder ValueCoder[T]) *ValkeyCache[T] { + self.coder = coder + return self +} + +// fullKey generates the full key with prefix. +func (self *ValkeyCache[T]) fullKey(key string) string { + if self.keyPrefix == "" { + return key + } + return self.keyPrefix + ":" + key +} + +// Set adds an item to the cache with no expiration. +func (self *ValkeyCache[T]) Set(ctx context.Context, key string, value T) error { + encoded, err := self.coder.Encode(value) + if err != nil { + return err + } + + cmd := self.client.B().Set().Key(self.fullKey(key)).Value(encoded).Build() + return self.client.Do(ctx, cmd).Error() +} + +// SetWithExpiration adds an item to the cache with an expiration time. +func (self *ValkeyCache[T]) SetWithExpiration(ctx context.Context, key string, value T, expiration time.Duration) error { + encoded, err := self.coder.Encode(value) + if err != nil { + return err + } + + cmd := self.client.B().Set().Key(self.fullKey(key)).Value(encoded).Ex(expiration).Build() + return self.client.Do(ctx, cmd).Error() +} + +// ErrKeyNotFound is returned when a key is not found in the cache. +var ErrKeyNotFound = errors.New("key not found in cache") + +// Get retrieves an item from the cache. +func (c *ValkeyCache[T]) Get(ctx context.Context, key string) (T, error) { + var value T + + cmd := c.client.B().Get().Key(c.fullKey(key)).Build() + result, err := c.client.Do(ctx, cmd).ToString() + if err != nil { + if err == valkey.Nil { + return value, ErrKeyNotFound + } + return value, err + } + + return c.coder.Decode(result) +} + +// GetWithTTL retrieves an item and its remaining TTL from the cache. +func (c *ValkeyCache[T]) GetWithTTL(ctx context.Context, key string) (T, time.Duration, error) { + var value T + fullKey := c.fullKey(key) + + // First check if the key exists + getCmd := c.client.B().Get().Key(fullKey).Build() + result, err := c.client.Do(ctx, getCmd).ToString() + if err != nil { + if err == valkey.Nil { + return value, 0, ErrKeyNotFound + } + return value, 0, err + } + + // Get TTL + ttlCmd := c.client.B().Ttl().Key(fullKey).Build() + ttlSeconds, err := c.client.Do(ctx, ttlCmd).ToInt64() + if err != nil { + return value, 0, err + } + + // Convert TTL to duration + var ttl time.Duration + if ttlSeconds > 0 { + ttl = time.Duration(ttlSeconds) * time.Second + } else if ttlSeconds == -1 { + // Key exists but has no expiration + ttl = -1 + } else { + // Key doesn't exist or is about to expire + return value, 0, ErrKeyNotFound + } + + // Decode value + value, err = c.coder.Decode(result) + if err != nil { + return value, 0, err + } + + return value, ttl, nil +} + +// Delete removes an item from the cache. +func (c *ValkeyCache[T]) Delete(ctx context.Context, key string) error { + cmd := c.client.B().Del().Key(c.fullKey(key)).Build() + return c.client.Do(ctx, cmd).Error() +} + +// DeleteAll removes all items with the cache's prefix. +func (c *ValkeyCache[T]) DeleteAll(ctx context.Context) error { + if c.keyPrefix == "" { + return errors.New("cannot delete all keys without a prefix") + } + + // Find all keys with this prefix + pattern := c.keyPrefix + ":*" + keysCmd := c.client.B().Keys().Pattern(pattern).Build() + keys, err := c.client.Do(ctx, keysCmd).AsStrSlice() + if err != nil { + return err + } + + if len(keys) == 0 { + return nil + } + + // Delete all found keys + delCmd := c.client.B().Del().Key(keys...).Build() + return c.client.Do(ctx, delCmd).Error() +} + +// Exists checks if a key exists in the cache. +func (c *ValkeyCache[T]) Exists(ctx context.Context, key string) (bool, error) { + cmd := c.client.B().Exists().Key(c.fullKey(key)).Build() + result, err := c.client.Do(ctx, cmd).ToInt64() + if err != nil { + return false, err + } + return result > 0, nil +} + +// Keys returns all keys with the cache's prefix. +func (c *ValkeyCache[T]) Keys(ctx context.Context) ([]string, error) { + var pattern string + if c.keyPrefix == "" { + pattern = "*" + } else { + pattern = c.keyPrefix + ":*" + } + + cmd := c.client.B().Keys().Pattern(pattern).Build() + keys, err := c.client.Do(ctx, cmd).AsStrSlice() + if err != nil { + return nil, err + } + + // Remove prefix from keys for consistency + if c.keyPrefix != "" { + prefixLen := len(c.keyPrefix) + 1 // +1 for the colon + for i, key := range keys { + keys[i] = key[prefixLen:] + } + } + + return keys, nil +} + +// GetAll retrieves all items from the cache. +func (c *ValkeyCache[T]) GetAll(ctx context.Context) (map[string]T, error) { + keys, err := c.Keys(ctx) + if err != nil { + return nil, err + } + + result := make(map[string]T) + for _, key := range keys { + value, err := c.Get(ctx, key) + if err != nil && !errors.Is(err, ErrKeyNotFound) { + return nil, err + } + if !errors.Is(err, ErrKeyNotFound) { + result[key] = value + } + } + + return result, nil +} + +// Increment increments a numeric value stored at the given key. +// Only works for numeric types like int, int64, float64. +func Increment[T ~int | ~int64 | ~float64]( + ctx context.Context, + cache *ValkeyCache[T], + key string, + increment T) (T, error) { + + var result T + fullKey := cache.fullKey(key) + + // Use INCRBY for integers and INCRBYFLOAT for floats + var cmd valkey.Completed + switch any(increment).(type) { + case int, int64: + cmd = cache.client.B().Incrby().Key(fullKey).Increment(int64(increment)).Build() + val, err := cache.client.Do(ctx, cmd).ToInt64() + if err != nil { + return result, err + } + return T(val), nil + case float64: + cmd = cache.client.B().Incrbyfloat().Key(fullKey).Increment(float64(increment)).Build() + val, err := cache.client.Do(ctx, cmd).ToFloat64() + if err != nil { + return result, err + } + return T(val), nil + default: + return result, fmt.Errorf("unsupported type for increment") + } +} diff --git a/internal/database/valkey_cache_test.go b/internal/database/valkey_cache_test.go new file mode 100644 index 00000000..7310d38e --- /dev/null +++ b/internal/database/valkey_cache_test.go @@ -0,0 +1,497 @@ +package database + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/valkey-io/valkey-go/mock" + "go.uber.org/mock/gomock" +) + +// User is a test struct for cache storage +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +// CacheSuite is a test suite for the Cache implementation +type CacheSuite struct { + suite.Suite + ctrl *gomock.Controller + mockClient *mock.Client + stringCache *ValkeyCache[string] + intCache *ValkeyCache[int] + userCache *ValkeyCache[User] + ctx context.Context +} + +// SetupTest runs before each test +func (s *CacheSuite) SetupTest() { + s.ctrl = gomock.NewController(s.T()) + s.mockClient = mock.NewClient(s.ctrl) + s.stringCache = NewCache[string](s.mockClient, "test") + s.intCache = NewCache[int](s.mockClient, "int-test") + s.userCache = NewCache[User](s.mockClient, "user-test") + s.ctx = context.Background() +} + +// TearDownTest cleans up after each test +func (s *CacheSuite) TearDownTest() { + s.ctrl.Finish() +} + +// TestSet tests the Set method +func (s *CacheSuite) TestSet() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("SET", "test:key1", "\"value1\"")). // Include the value in the match + Return(mock.Result(mock.ValkeyString("OK"))) + + // Call method + err := s.stringCache.Set(s.ctx, "key1", "value1") + + // Assert + assert.NoError(s.T(), err) +} + +// TestSetWithExpiration tests the SetWithExpiration method +func (s *CacheSuite) TestSetWithExpiration() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("SET", "test:key1", "\"value1\"", "EX", "600")). + Return(mock.Result(mock.ValkeyString("OK"))) + + // Call method + err := s.stringCache.SetWithExpiration(s.ctx, "key1", "value1", 10*time.Minute) + + // Assert + assert.NoError(s.T(), err) +} + +// TestGet tests the Get method +func (s *CacheSuite) TestGet() { + // Set up expectation + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyString("\"value1\""))) // Note the extra quotes for JSON + + // Call method + value, err := s.stringCache.Get(s.ctx, "key1") + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), "value1", value) +} + +// TestGet_NotFound tests the Get method when key is not found +func (s *CacheSuite) TestGet_NotFound() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("GET", "test:nonexistent")). + Return(mock.Result(mock.ValkeyNil())) + + // Call method + _, err := s.stringCache.Get(s.ctx, "nonexistent") + + // Assert + assert.Error(s.T(), err) + assert.True(s.T(), errors.Is(err, ErrKeyNotFound)) +} + +// TestGetWithTTL tests the GetWithTTL method +func (s *CacheSuite) TestGetWithTTL() { + // Set up expectation for GET + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("GET", "test:key1")). + Return(mock.Result(mock.ValkeyString("\"value1\""))) + + // Set up expectation for TTL + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("TTL", "test:key1")). + Return(mock.Result(mock.ValkeyInt64(300))) // 5 minutes in seconds + + // Call method + value, ttl, err := s.stringCache.GetWithTTL(s.ctx, "key1") + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), "value1", value) + assert.Equal(s.T(), 300*time.Second, ttl) +} + +// TestGetWithTTL_NoExpiry tests the GetWithTTL method for keys with no expiration +func (s *CacheSuite) TestGetWithTTL_NoExpiry() { + // Set up expectation for GET + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("GET", "test:key1")). + Return(mock.Result(mock.ValkeyString("\"value1\""))) + + // Set up expectation for TTL (return -1 for keys with no expiry) + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("TTL", "test:key1")). + Return(mock.Result(mock.ValkeyInt64(-1))) + + // Call method + value, ttl, err := s.stringCache.GetWithTTL(s.ctx, "key1") + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), "value1", value) + assert.Equal(s.T(), time.Duration(-1), ttl) // -1 indicates no expiry +} + +// TestDelete tests the Delete method +func (s *CacheSuite) TestDelete() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("DEL", "test:key1")). + Return(mock.Result(mock.ValkeyInt64(1))) + + // Call method + err := s.stringCache.Delete(s.ctx, "key1") + + // Assert + assert.NoError(s.T(), err) +} + +// TestDeleteAll tests the DeleteAll method +func (s *CacheSuite) TestDeleteAll() { + // Set up expectation for KEYS + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("KEYS", "test:*")). + Return(mock.Result(mock.ValkeyArray( + mock.ValkeyString("test:key1"), + mock.ValkeyString("test:key2"), + ))) + + // Set up expectation for DEL + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("DEL", "test:key1", "test:key2")). + Return(mock.Result(mock.ValkeyInt64(2))) + + // Call method + err := s.stringCache.DeleteAll(s.ctx) + + // Assert + assert.NoError(s.T(), err) +} + +// TestDeleteAll_NoKeys tests the DeleteAll method when no keys match +func (s *CacheSuite) TestDeleteAll_NoKeys() { + // Set up expectation for KEYS (empty result) + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("KEYS", "test:*")). + Return(mock.Result(mock.ValkeyArray())) + + // Call method + err := s.stringCache.DeleteAll(s.ctx) + + // Assert + assert.NoError(s.T(), err) +} + +// TestDeleteAll_NoPrefix tests the DeleteAll method when no prefix is set +func (s *CacheSuite) TestDeleteAll_NoPrefix() { + // Create cache with no prefix + noPrefix := NewCache[string](s.mockClient, "") + + // Call method + err := noPrefix.DeleteAll(s.ctx) + + // Assert + assert.Error(s.T(), err) + assert.Contains(s.T(), err.Error(), "without a prefix") +} + +// TestExists tests the Exists method +func (s *CacheSuite) TestExists() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("EXISTS", "test:key1")). + Return(mock.Result(mock.ValkeyInt64(1))) + + // Call method + exists, err := s.stringCache.Exists(s.ctx, "key1") + + // Assert + assert.NoError(s.T(), err) + assert.True(s.T(), exists) +} + +// TestExists_NotFound tests the Exists method when key doesn't exist +func (s *CacheSuite) TestExists_NotFound() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("EXISTS", "test:nonexistent")). + Return(mock.Result(mock.ValkeyInt64(0))) + + // Call method + exists, err := s.stringCache.Exists(s.ctx, "nonexistent") + + // Assert + assert.NoError(s.T(), err) + assert.False(s.T(), exists) +} + +// TestKeys tests the Keys method +func (s *CacheSuite) TestKeys() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("KEYS", "test:*")). + Return(mock.Result(mock.ValkeyArray( + mock.ValkeyString("test:key1"), + mock.ValkeyString("test:key2"), + ))) + + // Call method + keys, err := s.stringCache.Keys(s.ctx) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), []string{"key1", "key2"}, keys) +} + +// TestKeys_NoPrefix tests the Keys method when no prefix is set +func (s *CacheSuite) TestKeys_NoPrefix() { + // Create cache with no prefix + noPrefix := NewCache[string](s.mockClient, "") + + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("KEYS", "*")). + Return(mock.Result(mock.ValkeyArray( + mock.ValkeyString("key1"), + mock.ValkeyString("key2"), + ))) + + // Call method + keys, err := noPrefix.Keys(s.ctx) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), []string{"key1", "key2"}, keys) +} + +// TestGetAll tests the GetAll method +func (s *CacheSuite) TestGetAll() { + // Set up expectation for KEYS - don't add extra quotes to the keys + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyArray( + mock.ValkeyString("test:key1"), + mock.ValkeyString("test:key2"), + ))) + + // Set up expectations for GET calls + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyString("\"value1\""))) + + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyString("\"value2\""))) + + // Call method + items, err := s.stringCache.GetAll(s.ctx) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), map[string]string{ + "key1": "value1", + "key2": "value2", + }, items) +} + +// TestGetAll_WithMissingKey tests the GetAll method when one key is missing +func (s *CacheSuite) TestGetAll_WithMissingKey() { + // Set up expectation for KEYS + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("KEYS", "test:*")). + Return(mock.Result(mock.ValkeyArray( + mock.ValkeyString("test:key1"), + mock.ValkeyString("test:key2"), + ))) + + // Set up expectations for GET calls + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("GET", "test:key1")). + Return(mock.Result(mock.ValkeyString("\"value1\""))) + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("GET", "test:key2")). + Return(mock.Result(mock.ValkeyNil())) + + // Call method + items, err := s.stringCache.GetAll(s.ctx) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), map[string]string{ + "key1": "value1", + }, items) +} + +// TestStructEncoding tests encoding and decoding of struct values +func (s *CacheSuite) TestStructEncoding() { + user := User{ID: 1, Name: "John", Email: "john@example.com"} + userJSON, _ := json.Marshal(user) + + // Test Set with a struct - using gomock.Any() for arguments to avoid matching issues + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyString("OK"))) + + err := s.userCache.Set(s.ctx, "user1", user) + assert.NoError(s.T(), err) + + // Test Get with a struct + s.mockClient.EXPECT(). + Do(gomock.Any(), gomock.Any()). + Return(mock.Result(mock.ValkeyString(string(userJSON)))) + + retrievedUser, err := s.userCache.Get(s.ctx, "user1") + assert.NoError(s.T(), err) + assert.Equal(s.T(), user, retrievedUser) +} + +// TestIncrementInt tests the Increment function with int type +func (s *CacheSuite) TestIncrementInt() { + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("INCRBY", "int-test:counter", "5")). + Return(mock.Result(mock.ValkeyInt64(10))) + + // Call method + newValue, err := Increment(s.ctx, s.intCache, "counter", 5) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), 10, newValue) +} + +// TestIncrementFloat64 tests the Increment function with float64 type +func (s *CacheSuite) TestIncrementFloat64() { + // Create a float cache + floatCache := NewCache[float64](s.mockClient, "float-test") + + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("INCRBYFLOAT", "float-test:counter", "2.5")). + Return(mock.Result(mock.ValkeyFloat64(15.5))) + + // Call method + newValue, err := Increment(s.ctx, floatCache, "counter", 2.5) + + // Assert + assert.NoError(s.T(), err) + assert.Equal(s.T(), 15.5, newValue) +} + +// TestStringCache tests the specialized string cache +func (s *CacheSuite) TestStringCache() { + // Create a string cache + stringCache := NewStringCache(s.mockClient, "string-test") + + // Verify we're using StringValueCoder + assert.IsType(s.T(), StringValueCoder{}, stringCache.coder) + + // Set up expectation + s.mockClient.EXPECT(). + Do(s.ctx, mock.Match("SET", "string-test:key1", "direct-string")). + Return(mock.Result(mock.ValkeyString("\"OK\""))) + + // Call method + err := stringCache.Set(s.ctx, "key1", "direct-string") + + // Assert + assert.NoError(s.T(), err) +} + +// Run the test suite +func TestCacheSuite(t *testing.T) { + suite.Run(t, new(CacheSuite)) +} + +// Test the JSONValueCoder directly +func TestJSONValueCoder(t *testing.T) { + coder := JSONValueCoder[User]{} + + // Test encoding + user := User{ID: 123, Name: "Alice", Email: "alice@example.com"} + encoded, err := coder.Encode(user) + assert.NoError(t, err) + + // Verify it's valid JSON + var decodedMap map[string]interface{} + err = json.Unmarshal([]byte(encoded), &decodedMap) + assert.NoError(t, err) + assert.Equal(t, float64(123), decodedMap["id"]) + assert.Equal(t, "Alice", decodedMap["name"]) + assert.Equal(t, "alice@example.com", decodedMap["email"]) + + // Test decoding + decoded, err := coder.Decode(encoded) + assert.NoError(t, err) + assert.Equal(t, user, decoded) + + // Test decoding invalid JSON + _, err = coder.Decode("invalid json") + assert.Error(t, err) +} + +// Test the StringValueCoder directly +func TestStringValueCoder(t *testing.T) { + coder := StringValueCoder{} + + // Test encoding + encoded, err := coder.Encode("hello world") + assert.NoError(t, err) + assert.Equal(t, "hello world", encoded) + + // Test decoding + decoded, err := coder.Decode(encoded) + assert.NoError(t, err) + assert.Equal(t, "hello world", decoded) +} + +// Test creating cache with custom coder +func TestWithCoder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mock.NewClient(ctrl) + cache := NewCache[string](mockClient, "test") + + // Create a custom coder + customCoder := StringValueCoder{} + + // Apply the custom coder + result := cache.WithCoder(customCoder) + + // Assert it returns self for chaining + assert.Same(t, cache, result) + + // Assert the coder was set + assert.Equal(t, customCoder, cache.coder) +} + +// Test fullKey method +func TestFullKey(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mock.NewClient(ctrl) + + // Cache with prefix + cache := NewCache[string](mockClient, "prefix") + assert.Equal(t, "prefix:key", cache.fullKey("key")) + + // Cache without prefix + cacheNoPrefix := NewCache[string](mockClient, "") + assert.Equal(t, "key", cacheNoPrefix.fullKey("key")) +} diff --git a/internal/github/manifest.go b/internal/github/manifest.go index 51dca09e..4207dcb8 100644 --- a/internal/github/manifest.go +++ b/internal/github/manifest.go @@ -35,15 +35,17 @@ type DefaultPermissions struct { } // CreateAppManifest generates the GitHub App manifest -func (g *GithubClient) CreateAppManifest(redirectUrl string) (*GitHubAppManifest, error) { +func (g *GithubClient) CreateAppManifest(redirectUrl string) (manifest *GitHubAppManifest, appName string, err error) { // Generate a random suffix suffixRand, err := utils.GenerateRandomSimpleID(5) if err != nil { - return nil, fmt.Errorf("failed to generate random suffix: %w", err) + return nil, "", fmt.Errorf("failed to generate random suffix: %w", err) } + appName = fmt.Sprintf("unbind-%s-%s", g.cfg.UnbindSuffix, suffixRand) + return &GitHubAppManifest{ - Name: fmt.Sprintf("unbind-%s-%s", g.cfg.UnbindSuffix, suffixRand), + Name: appName, Description: "Application to connect unbind with Github", URL: g.cfg.ExternalURL, HookAttributes: HookAttributes{ @@ -58,7 +60,7 @@ func (g *GithubClient) CreateAppManifest(redirectUrl string) (*GitHubAppManifest PullRequests: "read", }, DefaultEvents: []string{"push", "pull_request"}, - }, nil + }, appName, nil } // ManifestCodeConversion gets app configruation from github using the code diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 705488ef..0b84cda8 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -2,25 +2,25 @@ package middleware import ( "net/http" - "strings" "github.com/danielgtaylor/huma/v2" "github.com/unbindapp/unbind-api/internal/log" ) func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { - authHeader := ctx.Header("Authorization") - if authHeader == "" { - huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header required") - return - } - - if !strings.HasPrefix(authHeader, "Bearer ") { - huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") - return - } - - bearerToken := strings.TrimPrefix(authHeader, "Bearer ") + // authHeader := ctx.Header("Authorization") + // if authHeader == "" { + // huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header required") + // return + // } + + // if !strings.HasPrefix(authHeader, "Bearer ") { + // huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") + // return + // } + + // bearerToken := strings.TrimPrefix(authHeader, "Bearer ") + bearerToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3YTk3YjBjYTc2OGQ1ZjgwM2M1MzI4ZjU0MDlhMmQzZTYyNzA3ZmEifQ.eyJpc3MiOiJodHRwczovL2RleC51bmJpbmQuYXBwIiwic3ViIjoiQ2lReE1qTTBOVFkzT0MweE1qTTBMVFUyTnpndE1USXpOQzAxTmpjNE1USXpORFUyTnpnU0JXeHZZMkZzIiwiYXVkIjoidW5iaW5kLWFwaSIsImV4cCI6MTc0MTI3MTA3MiwiaWF0IjoxNzQxMjcxMDEyLCJhdF9oYXNoIjoiaWp2ZzduZEZ6dWN5VDV6bjhYRGdhUSIsImVtYWlsIjoiYWRtaW5AdW5iaW5kLmFwcCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiYWRtaW4ifQ.nmHl7yAUa5QNMAOPF-uCDaQsiObS3IEPQiA0l0cORDDhBNEFumbrbBIqEfPTlvUHCKwEBsyBamd6PZbqUNHyvoMmMnhoAds4Om10vR3MnWBWr28Do3rGkrkiH4hJbqoJT1r163X4LFEuiPgV1m3K6QlKPFTZAT-IFQNMP6ki1mJQpKXN2gHzpYBaqpum24WgCoOILyKpISzNuUfuF_db1vR18bXKMQ13URgmG9mo-F0pxQHiKJCCvTQLGPvJ2eJTYqZOHDUfGsFCvpgPsMiE3KRewB5pWtEE0xpmERorETdAZeE_nmakjJE5bfeM5xziqdnK5sUvphZsOOdCArz38g" token, err := m.verifier.Verify(ctx.Context(), bearerToken) if err != nil { huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Invalid token") diff --git a/internal/server/github.go b/internal/server/github.go index 6285a692..9b3091f2 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -29,7 +29,7 @@ type GithubCreateManifestResponse struct { // Create a manifest that the user can use to create a GitHub app func (s *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { // Create GitHub app manifest - manifest, err := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) + manifest, _, err := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) if err != nil { log.Error("Error creating github app manifest", "err", err) diff --git a/internal/server/github_apps.go b/internal/server/github_apps.go index 3d9cfad2..e4440b29 100644 --- a/internal/server/github_apps.go +++ b/internal/server/github_apps.go @@ -8,18 +8,15 @@ import ( "html/template" "net/http" "net/url" + "time" "github.com/danielgtaylor/huma/v2" "github.com/google/uuid" "github.com/unbindapp/unbind-api/internal/log" "github.com/unbindapp/unbind-api/internal/utils" + "github.com/valkey-io/valkey-go" ) -type GithubAppCreateInput struct { - Body struct { - } -} - type GithubAppCreateResponse struct { ContentType string `header:"Content-Type"` Body []byte @@ -57,14 +54,23 @@ func (s *Server) HandleGithubAppCreate(ctx context.Context, input *EmptyInput) ( ` // Build redirect - redirect, err := utils.JoinURLPaths(s.Cfg.ExternalURL, "/github/app/save") + redirect, err := utils.JoinURLPaths(s.Cfg.ExternalURL, "/webhook/github/app/save") if err != nil { log.Error("Error building redirect URL", "err", err) return nil, huma.Error500InternalServerError("Failed to build redirect URL") } // Create GitHub app manifest - manifest, err := s.GithubClient.CreateAppManifest(redirect) + manifest, appName, err := s.GithubClient.CreateAppManifest(redirect) + + // Create a unique state to identify this request + state := uuid.New().String() + err = s.StringCache.SetWithExpiration(ctx, appName, state, 30*time.Minute) + if err != nil { + log.Error("Error setting state in cache", "err", err) + return nil, huma.Error500InternalServerError("Failed to set state in cache") + } + githubUrl := fmt.Sprintf("%s/settings/apps/new", s.Cfg.GithubURL) if err != nil { @@ -115,9 +121,8 @@ func (s *Server) HandleGithubAppCreate(ctx context.Context, input *EmptyInput) ( // Connect the new github app to our instance, via manifest code exchange type HandleGithubAppSaveInput struct { - Body struct { - Code string `json:"code"` - } + Code string `query:"code" validate:"required"` + State string `query:"state" validate:"required"` } type HandleGithubAppSaveResponse struct { @@ -129,11 +134,25 @@ type HandleGithubAppSaveResponse struct { // Save github app and redirect to installation func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubAppSaveInput) (*HandleGithubAppSaveResponse, error) { // Exchange the code for tokens. - appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) + appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Code) if err != nil { return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange manifest code: %v", err)) } + // Verify state + state, err := s.StringCache.Get(ctx, appConfig.GetName()) + if err != nil { + if err == valkey.Nil { + return nil, huma.Error400BadRequest("Invalid state") + } + log.Error("Error getting state from cache", "err", err) + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to get state: %v", err)) + } + + if state != input.State { + return nil, huma.Error400BadRequest("Invalid state") + } + // Save the app config ghApp, err := s.Repository.CreateGithubApp(ctx, appConfig) if err != nil { @@ -141,9 +160,6 @@ func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubApp return nil, huma.Error500InternalServerError("Failed to save github app") } - // Create a state parameter to verify the callback - state := uuid.New().String() - // create a cookie that stores the state value cookie := &http.Cookie{ Name: "github_install_state", diff --git a/internal/server/server.go b/internal/server/server.go index efbeef2c..ebd11450 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ import ( "context" "github.com/unbindapp/unbind-api/config" + "github.com/unbindapp/unbind-api/internal/database" "github.com/unbindapp/unbind-api/internal/database/repository" "github.com/unbindapp/unbind-api/internal/github" "github.com/unbindapp/unbind-api/internal/kubeclient" @@ -20,6 +21,7 @@ type Server struct { OauthConfig *oauth2.Config GithubClient *github.GithubClient Repository *repository.Repository + StringCache *database.ValkeyCache[string] } // HealthCheck is your /health endpoint diff --git a/internal/utils/rando_test.go b/internal/utils/rand_test.go similarity index 100% rename from internal/utils/rando_test.go rename to internal/utils/rand_test.go From e753b26fc0e742f66f0f9667c73d6b05adcd8d9b Mon Sep 17 00:00:00 2001 From: Brandon Berhent Date: Thu, 6 Mar 2025 14:51:05 +0000 Subject: [PATCH 22/22] Notify on installation --- cmd/main.go | 1 + internal/middleware/auth.go | 26 ++++++++-------- internal/server/auth_callback.go | 4 +-- internal/server/github.go | 24 +++++++-------- internal/server/github_apps.go | 51 ++++++++++++++++++++++++------- internal/server/github_webhook.go | 43 ++++++++++++++++++++++---- internal/server/login.go | 4 +-- internal/server/server.go | 4 ++- internal/server/teams.go | 4 +-- internal/server/user.go | 2 +- 10 files changed, 113 insertions(+), 50 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index f0247bf7..d7a0783c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -72,6 +72,7 @@ func main() { }, GithubClient: github.NewGithubClient(cfg), StringCache: database.NewStringCache(client, "unbind"), + HttpClient: &http.Client{}, } // New chi router diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 0b84cda8..705488ef 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -2,25 +2,25 @@ package middleware import ( "net/http" + "strings" "github.com/danielgtaylor/huma/v2" "github.com/unbindapp/unbind-api/internal/log" ) func (m *Middleware) Authenticate(ctx huma.Context, next func(huma.Context)) { - // authHeader := ctx.Header("Authorization") - // if authHeader == "" { - // huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header required") - // return - // } - - // if !strings.HasPrefix(authHeader, "Bearer ") { - // huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") - // return - // } - - // bearerToken := strings.TrimPrefix(authHeader, "Bearer ") - bearerToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3YTk3YjBjYTc2OGQ1ZjgwM2M1MzI4ZjU0MDlhMmQzZTYyNzA3ZmEifQ.eyJpc3MiOiJodHRwczovL2RleC51bmJpbmQuYXBwIiwic3ViIjoiQ2lReE1qTTBOVFkzT0MweE1qTTBMVFUyTnpndE1USXpOQzAxTmpjNE1USXpORFUyTnpnU0JXeHZZMkZzIiwiYXVkIjoidW5iaW5kLWFwaSIsImV4cCI6MTc0MTI3MTA3MiwiaWF0IjoxNzQxMjcxMDEyLCJhdF9oYXNoIjoiaWp2ZzduZEZ6dWN5VDV6bjhYRGdhUSIsImVtYWlsIjoiYWRtaW5AdW5iaW5kLmFwcCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiYWRtaW4ifQ.nmHl7yAUa5QNMAOPF-uCDaQsiObS3IEPQiA0l0cORDDhBNEFumbrbBIqEfPTlvUHCKwEBsyBamd6PZbqUNHyvoMmMnhoAds4Om10vR3MnWBWr28Do3rGkrkiH4hJbqoJT1r163X4LFEuiPgV1m3K6QlKPFTZAT-IFQNMP6ki1mJQpKXN2gHzpYBaqpum24WgCoOILyKpISzNuUfuF_db1vR18bXKMQ13URgmG9mo-F0pxQHiKJCCvTQLGPvJ2eJTYqZOHDUfGsFCvpgPsMiE3KRewB5pWtEE0xpmERorETdAZeE_nmakjJE5bfeM5xziqdnK5sUvphZsOOdCArz38g" + authHeader := ctx.Header("Authorization") + if authHeader == "" { + huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header required") + return + } + + if !strings.HasPrefix(authHeader, "Bearer ") { + huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Authorization header must be a Bearer token") + return + } + + bearerToken := strings.TrimPrefix(authHeader, "Bearer ") token, err := m.verifier.Verify(ctx.Context(), bearerToken) if err != nil { huma.WriteErr(m.api, ctx, http.StatusUnauthorized, "Invalid token") diff --git a/internal/server/auth_callback.go b/internal/server/auth_callback.go index bdddc0e6..bb6b4456 100644 --- a/internal/server/auth_callback.go +++ b/internal/server/auth_callback.go @@ -25,14 +25,14 @@ type CallbackResponse struct { } // Callback handles the OAuth2 callback. -func (s *Server) Callback(ctx context.Context, in *CallbackInput) (*CallbackResponse, error) { +func (self *Server) Callback(ctx context.Context, in *CallbackInput) (*CallbackResponse, error) { // Validate the code parameter. if in.Code == "" { return nil, huma.Error400BadRequest("No code provided") } // Exchange the code for tokens. - oauth2Token, err := s.OauthConfig.Exchange(ctx, in.Code) + oauth2Token, err := self.OauthConfig.Exchange(ctx, in.Code) if err != nil { return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange token: %v", err)) } diff --git a/internal/server/github.go b/internal/server/github.go index 9b3091f2..25e1af4e 100644 --- a/internal/server/github.go +++ b/internal/server/github.go @@ -27,9 +27,9 @@ type GithubCreateManifestResponse struct { } // Create a manifest that the user can use to create a GitHub app -func (s *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { +func (self *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCreateManifestInput) (*GithubCreateManifestResponse, error) { // Create GitHub app manifest - manifest, _, err := s.GithubClient.CreateAppManifest(input.Body.RedirectURL) + manifest, _, err := self.GithubClient.CreateAppManifest(input.Body.RedirectURL) if err != nil { log.Error("Error creating github app manifest", "err", err) @@ -39,7 +39,7 @@ func (s *Server) HandleGithubManifestCreate(ctx context.Context, input *GithubCr // Create resp resp := &GithubCreateManifestResponse{} resp.Body.Manifest = manifest - resp.Body.PostURL = fmt.Sprintf("%s/settings/apps/new", s.Cfg.GithubURL) + resp.Body.PostURL = fmt.Sprintf("%s/settings/apps/new", self.Cfg.GithubURL) return resp, nil } @@ -56,15 +56,15 @@ type HandleGithubAppConnectResponse struct { } } -func (s *Server) HandleGithubAppConnect(ctx context.Context, input *HandleGithubAppConnectInput) (*HandleGithubAppConnectResponse, error) { +func (self *Server) HandleGithubAppConnect(ctx context.Context, input *HandleGithubAppConnectInput) (*HandleGithubAppConnectResponse, error) { // Exchange the code for tokens. - appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) + appConfig, err := self.GithubClient.ManifestCodeConversion(ctx, input.Body.Code) if err != nil { return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange manifest code: %v", err)) } // Save the app config - ghApp, err := s.Repository.CreateGithubApp(ctx, appConfig) + ghApp, err := self.Repository.CreateGithubApp(ctx, appConfig) if err != nil { log.Error("Error saving github app", "err", err) return nil, huma.Error500InternalServerError("Failed to save github app") @@ -85,8 +85,8 @@ type GithubAppListResponse struct { Body []*ent.GithubApp } -func (s *Server) HandleListGithubApps(ctx context.Context, input *GithubAppListInput) (*GithubAppListResponse, error) { - apps, err := s.Repository.GetGithubApps(ctx, input.WithInstallations) +func (self *Server) HandleListGithubApps(ctx context.Context, input *GithubAppListInput) (*GithubAppListResponse, error) { + apps, err := self.Repository.GetGithubApps(ctx, input.WithInstallations) if err != nil { log.Error("Error getting github apps", "err", err) return nil, huma.Error500InternalServerError("Failed to get github apps") @@ -106,8 +106,8 @@ type GithubAppInstallationListResponse struct { Body []*ent.GithubInstallation } -func (s *Server) HandleListGithubAppInstallations(ctx context.Context, input *GithubAppInstallationListInput) (*GithubAppInstallationListResponse, error) { - installations, err := s.Repository.GetGithubInstallationsByAppID(ctx, input.AppID) +func (self *Server) HandleListGithubAppInstallations(ctx context.Context, input *GithubAppInstallationListInput) (*GithubAppInstallationListResponse, error) { + installations, err := self.Repository.GetGithubInstallationsByAppID(ctx, input.AppID) if err != nil { log.Error("Error getting github installations", "err", err) return nil, huma.Error500InternalServerError("Failed to get github installations") @@ -129,9 +129,9 @@ type HandleGithubAppInstallResponse struct { Cookie string `header:"Set-Cookie"` } -func (s *Server) HandleGithubAppInstall(ctx context.Context, input *HandleGithubAppInstallInput) (*HandleGithubAppInstallResponse, error) { +func (self *Server) HandleGithubAppInstall(ctx context.Context, input *HandleGithubAppInstallInput) (*HandleGithubAppInstallResponse, error) { // Get the app - ghApp, err := s.Repository.GetGithubAppByID(ctx, input.AppID) + ghApp, err := self.Repository.GetGithubAppByID(ctx, input.AppID) if err != nil { if ent.IsNotFound(err) { return nil, huma.Error404NotFound("App not found") diff --git a/internal/server/github_apps.go b/internal/server/github_apps.go index e4440b29..c8987ff4 100644 --- a/internal/server/github_apps.go +++ b/internal/server/github_apps.go @@ -8,6 +8,7 @@ import ( "html/template" "net/http" "net/url" + "strconv" "time" "github.com/danielgtaylor/huma/v2" @@ -17,13 +18,17 @@ import ( "github.com/valkey-io/valkey-go" ) +type GithubAppCreateInput struct { + RedirectURL string `query:"redirect_url" validate:"required" doc:"The client URL to redirect to after the installation is finished"` +} + type GithubAppCreateResponse struct { ContentType string `header:"Content-Type"` Body []byte } // Handler to render GitHub page with form submission -func (s *Server) HandleGithubAppCreate(ctx context.Context, input *EmptyInput) (*GithubAppCreateResponse, error) { +func (self *Server) HandleGithubAppCreate(ctx context.Context, input *GithubAppCreateInput) (*GithubAppCreateResponse, error) { // Template for the GitHub form submission page tmpl := ` @@ -54,24 +59,31 @@ func (s *Server) HandleGithubAppCreate(ctx context.Context, input *EmptyInput) ( ` // Build redirect - redirect, err := utils.JoinURLPaths(s.Cfg.ExternalURL, "/webhook/github/app/save") + redirect, err := utils.JoinURLPaths(self.Cfg.ExternalURL, "/webhook/github/app/save") if err != nil { log.Error("Error building redirect URL", "err", err) return nil, huma.Error500InternalServerError("Failed to build redirect URL") } // Create GitHub app manifest - manifest, appName, err := s.GithubClient.CreateAppManifest(redirect) + manifest, appName, err := self.GithubClient.CreateAppManifest(redirect) // Create a unique state to identify this request state := uuid.New().String() - err = s.StringCache.SetWithExpiration(ctx, appName, state, 30*time.Minute) + err = self.StringCache.SetWithExpiration(ctx, appName, state, 30*time.Minute) if err != nil { log.Error("Error setting state in cache", "err", err) return nil, huma.Error500InternalServerError("Failed to set state in cache") } - githubUrl := fmt.Sprintf("%s/settings/apps/new", s.Cfg.GithubURL) + // Store redirect URL for this state + err = self.StringCache.SetWithExpiration(ctx, state, input.RedirectURL, 30*time.Minute) + if err != nil { + log.Error("Error setting redirect URL in cache", "err", err) + return nil, huma.Error500InternalServerError("Failed to set redirect URL in cache") + } + + githubUrl := fmt.Sprintf("%s/settings/apps/new", self.Cfg.GithubURL) if err != nil { log.Error("Error creating github app manifest", "err", err) @@ -132,15 +144,15 @@ type HandleGithubAppSaveResponse struct { } // Save github app and redirect to installation -func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubAppSaveInput) (*HandleGithubAppSaveResponse, error) { +func (self *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubAppSaveInput) (*HandleGithubAppSaveResponse, error) { // Exchange the code for tokens. - appConfig, err := s.GithubClient.ManifestCodeConversion(ctx, input.Code) + appConfig, err := self.GithubClient.ManifestCodeConversion(ctx, input.Code) if err != nil { return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to exchange manifest code: %v", err)) } // Verify state - state, err := s.StringCache.Get(ctx, appConfig.GetName()) + state, err := self.StringCache.Get(ctx, appConfig.GetName()) if err != nil { if err == valkey.Nil { return nil, huma.Error400BadRequest("Invalid state") @@ -153,8 +165,25 @@ func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubApp return nil, huma.Error400BadRequest("Invalid state") } + // Get redirect URL from cache + redirectURL, err := self.StringCache.Get(ctx, input.State) + if err != nil { + if err == valkey.Nil { + return nil, huma.Error400BadRequest("Invalid state") + } + log.Error("Error getting redirect URL from cache", "err", err) + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to get redirect URL: %v", err)) + } + // Erase redirect URL and re-save it with app ID + err = self.StringCache.Delete(ctx, input.State) + if err != nil { + log.Error("Error deleting redirect URL from cache", "err", err) + return nil, huma.Error500InternalServerError(fmt.Sprintf("Failed to delete redirect URL: %v", err)) + } + err = self.StringCache.SetWithExpiration(ctx, strconv.Itoa(int(appConfig.GetID())), redirectURL, 30*time.Minute) + // Save the app config - ghApp, err := s.Repository.CreateGithubApp(ctx, appConfig) + ghApp, err := self.Repository.CreateGithubApp(ctx, appConfig) if err != nil { log.Error("Error saving github app", "err", err) return nil, huma.Error500InternalServerError("Failed to save github app") @@ -171,7 +200,7 @@ func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubApp } // Redirect URL - this is where GitHub will send users to install your app - redirectURL := fmt.Sprintf( + installationURL := fmt.Sprintf( "https://github.com/settings/apps/%s/installations/new?state=%s", url.QueryEscape(ghApp.Name), url.QueryEscape(state), @@ -179,7 +208,7 @@ func (s *Server) HandleGithubAppSave(ctx context.Context, input *HandleGithubApp return &HandleGithubAppSaveResponse{ Status: http.StatusTemporaryRedirect, - Url: redirectURL, + Url: installationURL, Cookie: cookie.String(), }, nil } diff --git a/internal/server/github_webhook.go b/internal/server/github_webhook.go index 5f87f643..ccce615a 100644 --- a/internal/server/github_webhook.go +++ b/internal/server/github_webhook.go @@ -2,6 +2,9 @@ package server import ( "context" + "net/http" + "net/url" + "strconv" "strings" "github.com/danielgtaylor/huma/v2" @@ -23,9 +26,9 @@ type GithubWebhookInput struct { type GithubWebhookOutput struct { } -func (s *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookInput) (*GithubWebhookOutput, error) { +func (self *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookInput) (*GithubWebhookOutput, error) { // Since we may have multiple apps, we want to validate against every webhook secret to see if it belongs to any of our apps - ghApps, err := s.Repository.GetGithubApps(ctx, false) + ghApps, err := self.Repository.GetGithubApps(ctx, false) if err != nil { log.Error("Error getting github apps", "err", err) return nil, huma.Error500InternalServerError("Failed to get github apps") @@ -101,7 +104,7 @@ func (s *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookIn } // Create or update installation in database - _, err = s.Repository.UpsertGithubInstallation( + _, err = self.Repository.UpsertGithubInstallation( ctx, installationID, installation.GetAppID(), @@ -123,26 +126,54 @@ func (s *Server) HandleGithubWebhook(ctx context.Context, input *GithubWebhookIn case "deleted": // Mark as inactive instead of deleting - _, err := s.Repository.SetInstallationActive(ctx, installationID, false) + _, err := self.Repository.SetInstallationActive(ctx, installationID, false) if err != nil { log.Error("Error setting installation as inactive", "err", err) return nil, huma.Error500InternalServerError("Failed to set installation as inactive") } case "suspended": - _, err := s.Repository.SetInstallationSuspended(ctx, installationID, true) + _, err := self.Repository.SetInstallationSuspended(ctx, installationID, true) if err != nil { log.Error("Error setting installation as suspended", "err", err) return nil, huma.Error500InternalServerError("Failed to set installation as suspended") } case "unsuspended": - _, err := s.Repository.SetInstallationSuspended(ctx, installationID, false) + _, err := self.Repository.SetInstallationSuspended(ctx, installationID, false) if err != nil { log.Error("Error setting installation as unsuspended", "err", err) return nil, huma.Error500InternalServerError("Failed to set installation as unsuspended") } } + + // Get Client redirect URL + redirectURL, err := self.StringCache.Get(ctx, strconv.Itoa(int(installation.GetAppID()))) + if err != nil { + log.Warn("Error getting redirect URL from cache", "err", err) + } + + if redirectURL != "" { + // Trigger a request to the client with installation completed + notifyUrl, err := url.Parse(redirectURL) + if err != nil { + log.Warn("Error parsing redirect URL", "err", err) + return &GithubWebhookOutput{}, nil + } + q := notifyUrl.Query() + q.Add("installationID", strconv.Itoa(int(installationID))) + notifyUrl.RawQuery = q.Encode() + req, err := http.NewRequestWithContext(ctx, "GET", notifyUrl.String(), nil) + if err != nil { + log.Warn("Error creating notification request", "err", err) + return &GithubWebhookOutput{}, nil + } + _, err = self.HttpClient.Do(req) + if err != nil { + log.Warn("Error notifying client of installation", "err", err, "url", notifyUrl.String()) + return &GithubWebhookOutput{}, nil + } + } } return &GithubWebhookOutput{}, nil diff --git a/internal/server/login.go b/internal/server/login.go index db084bb7..86a89b8d 100644 --- a/internal/server/login.go +++ b/internal/server/login.go @@ -16,12 +16,12 @@ type OauthLoginResponse struct { } // Login handles the OAuth login redirect. -func (s *Server) Login(ctx context.Context, _ *EmptyInput) (*OauthLoginResponse, error) { +func (self *Server) Login(ctx context.Context, _ *EmptyInput) (*OauthLoginResponse, error) { // Generate a random state value for CSRF protection. state := uuid.New().String() // Build the OAuth2 authentication URL with the state. - authURL := s.OauthConfig.AuthCodeURL(state, oauth2.AccessTypeOnline) + authURL := self.OauthConfig.AuthCodeURL(state, oauth2.AccessTypeOnline) // Create a cookie that stores the state value. cookie := &http.Cookie{ diff --git a/internal/server/server.go b/internal/server/server.go index ebd11450..59753207 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "net/http" "github.com/unbindapp/unbind-api/config" "github.com/unbindapp/unbind-api/internal/database" @@ -22,6 +23,7 @@ type Server struct { GithubClient *github.GithubClient Repository *repository.Repository StringCache *database.ValkeyCache[string] + HttpClient *http.Client } // HealthCheck is your /health endpoint @@ -31,7 +33,7 @@ type HealthResponse struct { } } -func (s *Server) HealthCheck(ctx context.Context, _ *EmptyInput) (*HealthResponse, error) { +func (self *Server) HealthCheck(ctx context.Context, _ *EmptyInput) (*HealthResponse, error) { healthResponse := &HealthResponse{} healthResponse.Body.Status = "ok" return healthResponse, nil diff --git a/internal/server/teams.go b/internal/server/teams.go index d36b4925..d4ca20b5 100644 --- a/internal/server/teams.go +++ b/internal/server/teams.go @@ -15,8 +15,8 @@ type TeamResponse struct { } // ListTeams handles GET /teams -func (s *Server) ListTeams(ctx context.Context, _ *EmptyInput) (*TeamResponse, error) { - teams, err := s.KubeClient.GetUnbindTeams() +func (self *Server) ListTeams(ctx context.Context, _ *EmptyInput) (*TeamResponse, error) { + teams, err := self.KubeClient.GetUnbindTeams() if err != nil { log.Error("Error getting teams", "err", err) return nil, huma.Error500InternalServerError("Unable to retrieve teams") diff --git a/internal/server/user.go b/internal/server/user.go index 3ab2092d..1c6d48e5 100644 --- a/internal/server/user.go +++ b/internal/server/user.go @@ -15,7 +15,7 @@ type MeResponse struct { } // Me handles GET /me -func (s *Server) Me(ctx context.Context, _ *EmptyInput) (*MeResponse, error) { +func (self *Server) Me(ctx context.Context, _ *EmptyInput) (*MeResponse, error) { user, ok := ctx.Value("user").(*ent.User) if !ok {