Compare commits

...

3 Commits

Author SHA1 Message Date
2a5b90b530 Status checking 2025-03-01 13:22:38 +08:00
fe2e682267 ♻️ Replace logger 2025-03-01 13:07:57 +08:00
481404c4a2 🗑️ Clean up command related stuff
🚚 Move http package to web
2025-03-01 13:00:50 +08:00
26 changed files with 113 additions and 847 deletions

23
go.mod
View File

@ -6,8 +6,9 @@ require (
github.com/fatih/color v1.18.0
github.com/go-playground/validator/v10 v10.22.1
github.com/goccy/go-json v0.10.3
github.com/gofiber/contrib/fiberzerolog v1.0.2
github.com/gofiber/contrib/websocket v1.3.2
github.com/gofiber/fiber/v2 v2.52.5
github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/json-iterator/go v1.1.12
github.com/lib/pq v1.10.9
@ -17,9 +18,9 @@ require (
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/spf13/viper v1.19.0
github.com/valyala/fasthttp v1.57.0
github.com/valyala/fasthttp v1.59.0
go.etcd.io/etcd/client/v3 v3.5.16
golang.org/x/text v0.21.0
golang.org/x/text v0.22.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
gorm.io/datatypes v1.2.4
@ -48,10 +49,10 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -62,6 +63,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
@ -70,18 +72,17 @@ require (
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.2.4 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.16 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

47
go.sum
View File

@ -36,10 +36,12 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/contrib/fiberzerolog v1.0.2 h1:LMa/luarQVeINoRwZLHtLQYepLPDIwUNB5OmdZKk+s8=
github.com/gofiber/contrib/fiberzerolog v1.0.2/go.mod h1:aTPsgArSgxRWcUeJ/K6PiICz3mbQENR1QOR426QwOoQ=
github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg=
github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
@ -73,8 +75,8 @@ 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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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=
@ -85,8 +87,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -125,8 +128,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@ -155,14 +158,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU=
github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg=
github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -182,8 +183,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
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-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -192,25 +193,25 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
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/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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/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/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=

View File

@ -1,22 +0,0 @@
package directory
const (
CommandMethodGet = "get"
CommandMethodPut = "put"
CommandMethodPatch = "patch"
CommandMethodPost = "post"
CommandMethodDelete = "delete"
)
type Command struct {
// The unique identifier of the command, different method command can hold the same command id
ID string `json:"id"`
// The method of the command, such as get, post, others; inspired by RESTful design
Method string `json:"method"`
// The tags of the command will be used to invoke the pre-command middlewares and post-command middlewares
Tags []string `json:"tags"`
// The implementation of the command, the handler is the service that will be invoked
Handler []*ServiceInstance `json:"handler"`
RobinIndex uint `json:"robin_index"`
}

View File

@ -1,79 +0,0 @@
package directory
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/internal/kv"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"github.com/goccy/go-json"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
)
const CommandInfoKvPrefix = "nexus.command/"
func AddCommand(id, method string, tags []string, handler *ServiceInstance) error {
if tags == nil {
tags = make([]string, 0)
}
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
command := &Command{
ID: id,
Method: method,
Tags: tags,
Handler: []*ServiceInstance{handler},
}
command.Handler = lo.UniqBy(command.Handler, func(item *ServiceInstance) string {
return item.ID
})
commandJSON, err := json.Marshal(command)
if err != nil {
log.Printf("Error marshaling command: %v", err)
return nil
}
_, err = kv.Kv.Put(context.Background(), ky, string(commandJSON))
return err
}
func GetCommandHandler(id, method string) *ServiceInstance {
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
resp, err := kv.Kv.Get(context.Background(), ky)
if err != nil {
return nil
}
if len(resp.Kvs) == 0 {
return nil
}
var command Command
if err := json.Unmarshal(resp.Kvs[0].Value, &command); err != nil {
return nil
}
if len(command.Handler) == 0 {
return nil
}
idx := command.RobinIndex % uint(len(command.Handler))
command.RobinIndex = idx + 1
raw, err := json.Marshal(&command)
if err == nil {
_, _ = kv.Kv.Put(context.Background(), ky, string(raw))
}
return command.Handler[idx]
}
func RemoveCommand(id, method string) error {
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
_, err := kv.Kv.Delete(context.Background(), ky)
return err
}

View File

@ -1,115 +0,0 @@
package directory
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"net/http"
"time"
)
type CommandRpcServer struct {
proto.UnimplementedCommandProviderServer
}
func (c *CommandRpcServer) AddCommand(ctx context.Context, info *proto.CommandInfo) (*proto.AddCommandResponse, error) {
clientId, err := GetClientId(ctx)
if err != nil {
return nil, err
}
service := GetServiceInstance(clientId)
if service == nil {
return nil, status.Errorf(codes.NotFound, "service not found")
}
AddCommand(info.GetId(), info.GetMethod(), info.GetTags(), service)
return &proto.AddCommandResponse{
IsSuccess: true,
}, nil
}
func (c *CommandRpcServer) RemoveCommand(ctx context.Context, request *proto.CommandLookupRequest) (*proto.RemoveCommandResponse, error) {
RemoveCommand(request.GetId(), request.GetMethod())
return &proto.RemoveCommandResponse{
IsSuccess: true,
}, nil
}
func (c *CommandRpcServer) SendCommand(ctx context.Context, argument *proto.CommandArgument) (*proto.CommandReturn, error) {
id := argument.GetCommand()
method := argument.GetMethod()
handler := GetCommandHandler(id, method)
if handler == nil {
return &proto.CommandReturn{
IsDelivered: false,
Status: http.StatusNotFound,
ContentType: "text/plain+error",
Payload: []byte("command not found"),
}, nil
}
conn, err := handler.GetGrpcConn()
if err != nil {
return &proto.CommandReturn{
IsDelivered: false,
Status: http.StatusServiceUnavailable,
ContentType: "text/plain+error",
Payload: []byte("service unavailable"),
}, nil
}
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
out, err := proto.NewCommandProviderClient(conn).SendCommand(ctx, argument)
if err != nil {
return &proto.CommandReturn{
IsDelivered: true,
Status: http.StatusInternalServerError,
ContentType: "text/plain+error",
Payload: []byte(err.Error()),
}, nil
}
out.IsDelivered = true
return out, nil
}
func (c *CommandRpcServer) SendStreamCommand(g proto.CommandProvider_SendStreamCommandServer) error {
for {
pck, err := g.Recv()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
id := pck.GetCommand()
method := pck.GetMethod()
handler := GetCommandHandler(id, method)
if handler == nil {
return status.Errorf(codes.NotFound, "command not found")
}
conn, err := handler.GetGrpcConn()
ctx, cancel := context.WithTimeout(g.Context(), time.Second*10)
out, err := proto.NewCommandProviderClient(conn).SendCommand(ctx, pck)
cancel()
if err != nil {
_ = g.Send(&proto.CommandReturn{
IsDelivered: false,
Status: http.StatusInternalServerError,
ContentType: "text/plain+error",
Payload: []byte(err.Error()),
})
} else {
_ = g.Send(out)
}
}
}

View File

@ -1,8 +1,34 @@
package directory
import "github.com/rs/zerolog/log"
import (
"sync"
"github.com/rs/zerolog/log"
)
var statusOfServices = make(map[string]bool)
var statusLock sync.Mutex
func GetServiceStatus() map[string]bool {
out := make(map[string]bool)
for k, v := range statusOfServices {
out[k] = v
}
services := ListServiceInstance()
for _, service := range services {
if _, ok := out[service.Type]; !ok {
out[service.Type] = false
}
}
return out
}
func ValidateServices() {
statusLock.Lock()
defer statusLock.Unlock()
services := ListServiceInstance()
if len(services) == 0 {
return
@ -19,9 +45,12 @@ func ValidateServices() {
}
// Directly use the connect method to skip cache
if _, err := ConnectService(service); err != nil {
statusOfServices[service.Type] = false
_ = RemoveServiceInstance(service.ID)
log.Warn().Err(err).Str("id", service.ID).Str("addr", service.GrpcAddr).Msg("Unable connect to service, dropped...")
continue
} else {
statusOfServices[service.Type] = true
}
successCount++

View File

@ -1,9 +1,10 @@
package grpc
import (
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
"net"
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"google.golang.org/grpc/reflection"
@ -29,7 +30,6 @@ func NewServer() *Server {
}
proto.RegisterDirectoryServiceServer(server.srv, &directory.ServiceRpcServer{})
proto.RegisterCommandProviderServer(server.srv, &directory.CommandRpcServer{})
proto.RegisterDatabaseServiceServer(server.srv, server)
proto.RegisterStreamServiceServer(server.srv, server)
proto.RegisterAllocatorServiceServer(server.srv, server)

View File

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/internal/http/ws"
"git.solsynth.dev/hypernet/nexus/pkg/internal/web/ws"
"github.com/rs/zerolog/log"
"git.solsynth.dev/hypernet/nexus/pkg/proto"

View File

@ -1,67 +0,0 @@
package api
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"google.golang.org/grpc/metadata"
"strings"
"time"
)
func invokeCommand(c *fiber.Ctx) error {
command := c.Params("command")
method := strings.ToLower(c.Method())
handler := directory.GetCommandHandler(command, method)
if handler == nil {
return fiber.NewError(fiber.StatusNotFound, "command not found")
}
conn, err := handler.GetGrpcConn()
if err != nil {
return fiber.NewError(fiber.StatusServiceUnavailable, "service unavailable")
}
log.Debug().Str("id", command).Str("method", method).Msg("Invoking command from HTTP Gateway...")
var meta []string
meta = append(meta, "client_id", "http-gateway")
meta = append(meta, "net.ip", c.IP())
meta = append(meta, "http.user_agent", c.Get(fiber.HeaderUserAgent))
for k, v := range c.GetReqHeaders() {
meta = append(
meta,
strings.ToLower(fmt.Sprintf("header.%s", strings.ReplaceAll(k, "-", "_"))),
strings.Join(v, "\n"),
)
}
for k, v := range c.Queries() {
meta = append(
meta,
strings.ToLower(fmt.Sprintf("query.%s", strings.ReplaceAll(k, "-", "_"))),
v,
)
}
ctx := metadata.AppendToOutgoingContext(c.Context(), meta...)
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
out, err := proto.NewCommandProviderClient(conn).SendCommand(ctx, &proto.CommandArgument{
Command: command,
Method: method,
Payload: c.Body(),
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
c.Set(fiber.HeaderContentType, out.ContentType)
return c.Status(int(out.Status)).Send(out.Payload)
}
}

View File

@ -4,27 +4,28 @@ import (
pkg "git.solsynth.dev/hypernet/nexus/pkg/internal"
"git.solsynth.dev/hypernet/nexus/pkg/internal/auth"
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
"git.solsynth.dev/hypernet/nexus/pkg/internal/http/ws"
"git.solsynth.dev/hypernet/nexus/pkg/internal/web/ws"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
)
func MapAPIs(app *fiber.App) {
func MapControllers(app *fiber.App) {
app.Get("/check-ip", getClientIP)
app.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"api_level": pkg.ApiLevel,
"version": pkg.AppVersion,
"status": true,
})
})
app.Get("/directory/status", getServicesStatus)
app.Get("/directory/services", listExistsService)
// Some built-in public-accessible APIs
wellKnown := app.Group("/.well-known").Name("Well Known")
{
wellKnown.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"api_level": pkg.ApiLevel,
"version": pkg.AppVersion,
"status": true,
})
})
wellKnown.Get("/check-ip", getClientIP)
wellKnown.Get("/directory/services", listExistsService)
wellKnown.Get("/openid-configuration", func(c *fiber.Ctx) error {
service := directory.GetServiceInstanceByType(nex.ServiceTypeAuth)
if service == nil || service.HttpAddr == nil {
@ -44,12 +45,11 @@ func MapAPIs(app *fiber.App) {
// WatchTower administration APIs
wt := app.Group("/wt").Name("WatchTower").Use(auth.ValidatorMiddleware)
{
wt.Post("/maintenance/database", wtRunDbMaintenance)
wt.Post("/maintenance/database", watchRunDbMaintenance)
}
// Common websocket gateway
app.Get("/ws", auth.ValidatorMiddleware, websocket.New(ws.Listen))
app.All("/inv/:command", invokeCommand)
app.All("/cgi/:service/*", forwardService)
}

View File

@ -0,0 +1,10 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
"github.com/gofiber/fiber/v2"
)
func getServicesStatus(c *fiber.Ctx) error {
return c.JSON(directory.GetServiceStatus())
}

View File

@ -6,7 +6,7 @@ import (
"github.com/gofiber/fiber/v2"
)
func wtRunDbMaintenance(c *fiber.Ctx) error {
func watchRunDbMaintenance(c *fiber.Ctx) error {
if err := sec.EnsureGrantedPerm(c, "AdminOperateWatchTower", true); err != nil {
return err
}

View File

@ -1,30 +1,30 @@
package server
package web
import (
"time"
"git.solsynth.dev/hypernet/nexus/pkg/internal/auth"
"git.solsynth.dev/hypernet/nexus/pkg/internal/http/api"
"git.solsynth.dev/hypernet/nexus/pkg/internal/web/api"
"github.com/goccy/go-json"
"github.com/gofiber/contrib/fiberzerolog"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/idempotency"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type HTTPApp struct {
type WebApp struct {
app *fiber.App
}
func NewServer() *HTTPApp {
func NewServer() *WebApp {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hypernet.Nexus",
AppName: "Hypernet.Nexus",
ServerHeader: "HyperNet.Nexus",
AppName: "HyperNet.Nexus",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
@ -32,6 +32,10 @@ func NewServer() *HTTPApp {
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
app.Use(fiberzerolog.New(fiberzerolog.Config{
Logger: &log.Logger,
}))
app.Use(idempotency.New())
app.Use(cors.New(cors.Config{
AllowCredentials: true,
@ -41,11 +45,6 @@ func NewServer() *HTTPApp {
},
}))
app.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
app.Use(auth.ContextMiddleware)
app.Use(limiter.New(limiter.Config{
Max: viper.GetInt("rate_limit"),
@ -53,12 +52,12 @@ func NewServer() *HTTPApp {
LimiterMiddleware: limiter.SlidingWindow{},
}))
api.MapAPIs(app)
api.MapControllers(app)
return &HTTPApp{app}
return &WebApp{app}
}
func (v *HTTPApp) Listen() {
func (v *WebApp) Listen() {
if err := v.app.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}

View File

@ -9,10 +9,10 @@ import (
"git.solsynth.dev/hypernet/nexus/pkg/internal/auth"
"git.solsynth.dev/hypernet/nexus/pkg/internal/database"
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
server "git.solsynth.dev/hypernet/nexus/pkg/internal/http"
"git.solsynth.dev/hypernet/nexus/pkg/internal/kv"
"git.solsynth.dev/hypernet/nexus/pkg/internal/mq"
"git.solsynth.dev/hypernet/nexus/pkg/internal/watchtower"
"git.solsynth.dev/hypernet/nexus/pkg/internal/web"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"github.com/fatih/color"
@ -109,7 +109,7 @@ func main() {
go directory.ValidateServices()
// Server
go server.NewServer().Listen()
go web.NewServer().Listen()
// Grpc Server
go grpc.NewServer().Listen()
@ -117,6 +117,7 @@ func main() {
// Configure timed tasks
quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
quartz.AddFunc("@midnight", watchtower.RunDbMaintenance)
quartz.AddFunc("@every 5m", directory.ValidateServices)
quartz.Start()
// Messages

View File

@ -1,150 +0,0 @@
package nex
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/reflection"
"net"
"net/http"
"strconv"
"strings"
"time"
)
type CommandHandler func(ctx *CommandCtx) error
func GetCommandKey(id, method string) string {
return id + ":" + method
}
func (v *Conn) AddCommand(id, method string, tags []string, fn CommandHandler) error {
method = strings.ToLower(method)
dir := proto.NewCommandProviderClient(v.nexusConn)
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "client_id", v.Info.Id)
var addingMethodQueue []string
if method == "all" {
addingMethodQueue = []string{"get", "post", "put", "patch", "delete"}
} else {
addingMethodQueue = append(addingMethodQueue, method)
}
for _, method := range addingMethodQueue {
ky := GetCommandKey(id, method)
_, err := dir.AddCommand(ctx, &proto.CommandInfo{
Id: id,
Method: method,
Tags: tags,
})
if err == nil {
v.commandHandlers[ky] = fn
} else {
return err
}
}
return nil
}
type localCommandRpcServer struct {
conn *Conn
proto.UnimplementedCommandProviderServer
health.UnimplementedHealthServer
}
func (v localCommandRpcServer) SendCommand(ctx context.Context, argument *proto.CommandArgument) (*proto.CommandReturn, error) {
ky := GetCommandKey(argument.GetCommand(), argument.GetMethod())
if handler, ok := v.conn.commandHandlers[ky]; !ok {
return &proto.CommandReturn{
Status: http.StatusNotFound,
Payload: []byte(argument.GetCommand() + " not found"),
}, nil
} else {
cc := &CommandCtx{
requestBody: argument.GetPayload(),
statusCode: http.StatusOK,
}
if md, ok := metadata.FromIncomingContext(ctx); ok {
for k, v := range md {
var val any = nil
if len(v) == 1 {
if len(v[0]) != 0 {
if i, err := strconv.ParseInt(v[0], 10, 64); err == nil {
val = i
} else if b, err := strconv.ParseBool(v[0]); err == nil {
val = b
} else if f, err := strconv.ParseFloat(v[0], 64); err == nil {
val = f
}
layouts := []string{
time.RFC3339,
"2006-01-02 15:04:05", // Example: 2024-10-20 14:55:05
"2006-01-02", // Example: 2024-10-20
}
for _, layout := range layouts {
if t, err := time.Parse(layout, v[0]); err == nil {
val = t
}
}
if val == nil {
val = v[0]
}
} else {
val = v[0]
}
} else if len(v) > 1 {
val = v
}
cc.values.Store(k, val)
}
}
if err := handler(cc); err != nil {
return nil, err
} else {
return &proto.CommandReturn{
Status: int32(cc.statusCode),
ContentType: cc.contentType,
Payload: cc.responseBody,
}, nil
}
}
}
func (v localCommandRpcServer) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
return &health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}, nil
}
func (v localCommandRpcServer) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
for {
if server.Send(&health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}) != nil {
break
}
time.Sleep(1000 * time.Millisecond)
}
return nil
}
func (v *Conn) RunCommands(addr string) error {
v.commandServer = grpc.NewServer()
service := &localCommandRpcServer{conn: v}
proto.RegisterCommandProviderServer(v.commandServer, service)
health.RegisterHealthServer(v.commandServer, service)
reflection.Register(v.commandServer)
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return v.commandServer.Serve(listener)
}

View File

@ -1,90 +0,0 @@
package nex
import (
"fmt"
"github.com/goccy/go-json"
"net/http"
"sync"
)
type CommandCtx struct {
requestBody []byte
responseBody []byte
contentType string
statusCode int
values sync.Map
}
func CtxValueMustBe[T any](c *CommandCtx, key string) (T, error) {
if val, ok := c.values.Load(key); ok {
if v, ok := val.(T); ok {
return v, nil
}
}
var out T
if err := c.Write([]byte(fmt.Sprintf("value %s not found in type %T", key, out)), "text/plain+error", http.StatusBadRequest); err != nil {
return out, err
}
return out, fmt.Errorf("value %s not found", key)
}
func CtxValueShouldBe[T any](c *CommandCtx, key string, defaultValue T) T {
if val, ok := c.values.Load(key); ok {
if v, ok := val.(T); ok {
return v
}
}
return defaultValue
}
func (c *CommandCtx) Values() map[string]any {
duplicate := make(map[string]any)
c.values.Range(func(key, value any) bool {
duplicate[key.(string)] = value
return true
})
return duplicate
}
func (c *CommandCtx) ValueOrElse(key string, defaultValue any) any {
val, _ := c.values.Load(key)
if val == nil {
return defaultValue
}
return val
}
func (c *CommandCtx) Value(key string, newValue ...any) any {
if len(newValue) > 0 {
c.values.Store(key, newValue[0])
}
val, _ := c.values.Load(key)
return val
}
func (c *CommandCtx) Read() []byte {
return c.requestBody
}
func (c *CommandCtx) ReadJSON(out any) error {
return json.Unmarshal(c.requestBody, out)
}
func (c *CommandCtx) Write(data []byte, contentType string, statusCode ...int) error {
c.responseBody = data
c.contentType = contentType
if len(statusCode) > 0 {
c.statusCode = statusCode[0]
}
return nil
}
func (c *CommandCtx) JSON(data any, statusCode ...int) error {
raw, err := json.Marshal(data)
if err != nil {
return err
}
return c.Write(raw, "application/json", statusCode...)
}

View File

@ -1,60 +0,0 @@
package nex_test
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"net/http"
"testing"
"time"
)
func TestHandleCommand(t *testing.T) {
conn, err := nex.NewNexusConn("127.0.0.1:7001", &proto.ServiceInfo{
Id: "echo01",
Type: "echo",
Label: "Echo",
GrpcAddr: "127.0.0.1:6001",
HttpAddr: nil,
})
if err != nil {
t.Fatal(fmt.Errorf("unable to connect nexus: %v", err))
}
if err := conn.RegisterService(); err != nil {
t.Fatal(fmt.Errorf("unable to register service: %v", err))
}
err = conn.AddCommand("say.hi", "all", nil, func(ctx *nex.CommandCtx) error {
return ctx.Write([]byte("Hello, World!"), "text/plain", http.StatusOK)
})
if err != nil {
t.Fatal(fmt.Errorf("unable to add command: %v", err))
return
}
err = conn.AddCommand("echo", "all", nil, func(ctx *nex.CommandCtx) error {
t.Log("Received command: ", string(ctx.Read()))
return ctx.Write(ctx.Read(), "text/plain", http.StatusOK)
})
err = conn.AddCommand("echo.details", "all", nil, func(ctx *nex.CommandCtx) error {
return ctx.JSON(map[string]any{
"values": ctx.Values(),
"body": ctx.Read(),
}, http.StatusOK)
})
if err != nil {
t.Fatal(fmt.Errorf("unable to add command: %v", err))
return
}
go func() {
err := conn.RunCommands("0.0.0.0:6001")
if err != nil {
t.Error(fmt.Errorf("unable to run commands: %v", err))
return
}
}()
t.Log("Waiting 60 seconds for calling command...")
time.Sleep(time.Second * 60)
}

View File

@ -2,9 +2,10 @@ package nex
import (
"context"
"google.golang.org/grpc/metadata"
"time"
"google.golang.org/grpc/metadata"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@ -15,9 +16,6 @@ type Conn struct {
Addr string
Info *proto.ServiceInfo
commandServer *grpc.Server
commandHandlers map[string]CommandHandler
nexusConn *grpc.ClientConn
clientConn map[string]*grpc.ClientConn
}
@ -35,8 +33,6 @@ func NewNexusConn(addr string, info *proto.ServiceInfo) (*Conn, error) {
Addr: addr,
Info: info,
commandHandlers: make(map[string]CommandHandler),
nexusConn: conn,
clientConn: make(map[string]*grpc.ClientConn),
}, nil

View File

@ -1,130 +0,0 @@
package cruda
import (
"errors"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
"net/http"
)
type CrudAction func(v *CrudConn) nex.CommandHandler
func AddModel[T any](v *CrudConn, model T, id, prefix string, tags []string) error {
funcList := []CrudAction{cmdList[T], cmdGet[T], cmdCreate[T], cmdUpdate[T], cmdDelete[T]}
funcCmds := []string{".list", "", "", "", ""}
funcMethods := []string{"get", "get", "put", "patch", "delete"}
for idx, fn := range funcList {
if err := v.n.AddCommand(prefix+id+funcCmds[idx], funcMethods[idx], tags, fn(v)); err != nil {
return err
}
}
return nil
}
var validate = validator.New(validator.WithRequiredStructEnabled())
func cmdList[T any](c *CrudConn) nex.CommandHandler {
return func(ctx *nex.CommandCtx) error {
take := int(nex.CtxValueShouldBe[int64](ctx, "query.take", 10))
skip := int(nex.CtxValueShouldBe[int64](ctx, "query.skip", 0))
var str T
var count int64
if err := c.Db.Model(str).Count(&count).Error; err != nil {
return err
}
var out []T
if err := c.Db.Offset(skip).Limit(take).Find(&out).Error; err != nil {
return err
}
return ctx.JSON(map[string]any{
"count": count,
"data": out,
}, http.StatusOK)
}
}
func cmdGet[T any](c *CrudConn) nex.CommandHandler {
return func(ctx *nex.CommandCtx) error {
id, err := nex.CtxValueMustBe[int64](ctx, "query.id")
if err != nil {
return err
}
var out T
if err := c.Db.First(&out, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.Write([]byte(err.Error()), "text/plain", http.StatusNotFound)
}
return err
}
return ctx.JSON(out, http.StatusOK)
}
}
func cmdCreate[T any](c *CrudConn) nex.CommandHandler {
return func(ctx *nex.CommandCtx) error {
var payload T
if err := ctx.ReadJSON(&payload); err != nil {
return err
} else if err := validate.Struct(payload); err != nil {
return ctx.Write([]byte(err.Error()), "text/plain+error", http.StatusBadRequest)
}
if err := c.Db.Create(&payload).Error; err != nil {
return err
}
return ctx.JSON(payload, http.StatusOK)
}
}
func cmdUpdate[T any](c *CrudConn) nex.CommandHandler {
return func(ctx *nex.CommandCtx) error {
id, err := nex.CtxValueMustBe[int64](ctx, "query.id")
if err != nil {
return err
}
var payload T
if err := ctx.ReadJSON(&payload); err != nil {
return err
} else if err := validate.Struct(payload); err != nil {
return ctx.Write([]byte(err.Error()), "text/plain+error", http.StatusBadRequest)
}
var out T
if err := c.Db.Model(out).Where("id = ?", id).Updates(&payload).Error; err != nil {
return err
}
if err := c.Db.First(&out, "id = ?", id).Error; err != nil {
return err
}
return ctx.JSON(out, http.StatusOK)
}
}
func cmdDelete[T any](c *CrudConn) nex.CommandHandler {
return func(ctx *nex.CommandCtx) error {
id, err := nex.CtxValueMustBe[int64](ctx, "query.id")
if err != nil {
return err
}
var out T
if err := c.Db.Delete(&out, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.Write([]byte(err.Error()), "text/plain", http.StatusNotFound)
}
return err
}
return ctx.Write(nil, "text/plain", http.StatusOK)
}
}

View File

@ -1,58 +0,0 @@
package cruda_test
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"testing"
"time"
)
type Test struct {
cruda.BaseModel
Content string `json:"content" validate:"required"`
}
func TestCrudaCommand(t *testing.T) {
conn, err := nex.NewNexusConn("127.0.0.1:7001", &proto.ServiceInfo{
Id: "cruda01",
Type: "cruda",
Label: "CRUD Accelerator",
GrpcAddr: "127.0.0.1:6001",
HttpAddr: nil,
})
if err != nil {
t.Fatal(fmt.Errorf("unable to connect nexus: %v", err))
}
if err := conn.RegisterService(); err != nil {
t.Fatal(fmt.Errorf("unable to register service: %v", err))
}
cc := cruda.NewCrudaConn(conn)
dsn, err := cc.AllocDatabase("test")
if err != nil {
t.Fatal(fmt.Errorf("unable to allocate database: %v", err))
}
t.Log(fmt.Sprintf("Allocated database: %s", dsn))
if err := cruda.MigrateModel(cc, Test{}); err != nil {
t.Fatal(fmt.Errorf("unable to migrate database: %v", err))
}
if err := cruda.AddModel(cc, Test{}, "tm", "test.", nil); err != nil {
t.Fatal(fmt.Errorf("unable to add commands: %v", err))
}
go func() {
err := conn.RunCommands("0.0.0.0:6001")
if err != nil {
t.Error(fmt.Errorf("unable to run commands: %v", err))
return
}
}()
t.Log("Waiting 180 seconds for calling command...")
time.Sleep(time.Second * 180)
}