more updates

This commit is contained in:
Tigor Hutasuhut 2024-08-05 23:06:32 +07:00
parent 856c846dbd
commit b23ac7617e
22 changed files with 783 additions and 30 deletions

3
.gitignore vendored
View file

@ -23,6 +23,3 @@ go.work
.direnv
bin/
gen/
*.db

View file

@ -1,17 +1,18 @@
.ONESHELL:
export PATH := $(shell pwd)/bin:$(shell pwd)/web/node_modules/.bin:$(PATH)
export GOOSE_DRIVER=sqlite3
export GOOSE_DBSTRING=./data.db
export GOOSE_MIGRATION_DIR=schemas/migrations
export GOOSE_DRIVER ?= sqlite3
export GOOSE_DBSTRING ?= ./go/data.db
export GOOSE_MIGRATION_DIR ?= schemas/migrations
build: generate-go generate-web
go build -o bin/bluemage ./go/cmd/bluemage/main.go
generate-go:
generate-go: migrate
rm -rf go/gen
cd ./schemas/proto
buf generate --template buf.gen.go.yaml .
(cd ./schemas/proto && buf generate --template buf.gen.go.yaml .)
(cd go/gen && bobgen-sqlite --config ../bobgen.yaml)
(cd go && goverter gen -g 'output:file ../gen/converter/converter.go' -g 'output:package github.com/tigorlazuardi/bluemage/go/gen/converter' ./converter)
generate-web:
rm -rf web/gen

View file

@ -30,6 +30,29 @@
'';
vendorHash = "sha256-uQ1qKZLRwsgXKqSAERSqf+1cYKp6MTeVbfGs+qcdakE=";
};
bobgen-sqlite = pkgs.buildGoModule rec {
name = "bobgen-sqlite";
version = "0.28.1";
src = pkgs.fetchFromGitHub {
owner = "stephenafamo";
repo = "bob";
rev = "v${version}";
sha256 = "sha256-iLcSY5BBmjuICsIC9u6wyrp7elDt4yY8Ji7UWmsJ688=";
};
nativeBuildInputs = [ pkgs.go ];
buildPhase = ''
runHook preBuild
go build -o bobgen-sqlite ./gen/bobgen-sqlite
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp bobgen-sqlite $out/bin
runHook postInstall
'';
vendorHash = "sha256-9vLcyCAhW/Dq0zV8FhM0T3Ofn/8XsUhV5KlphNidGDw=";
};
in
{
devShell.${system} = pkgs.mkShell {
@ -45,7 +68,9 @@
protoc-gen-go
protoc-gen-connect-go
protoc-gen-validate
impl
goverter
bobgen-sqlite
];
};
};

24
go.mod
View file

@ -1,3 +1,27 @@
module github.com/tigorlazuardi/bluemage
go 1.22.5
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2
connectrpc.com/connect v1.16.2
github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536
github.com/bufbuild/protovalidate-go v0.6.3
github.com/jaswdr/faker/v2 v2.3.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/stephenafamo/bob v0.28.1
google.golang.org/protobuf v1.34.2
)
require (
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/stephenafamo/scan v0.4.2 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
)

119
go.sum Normal file
View file

@ -0,0 +1,119 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2 h1:SZRVx928rbYZ6hEKUIN+vtGDkl7uotABRWGY4OAg5gM=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240717164558-a6c49f84cc0f.2/go.mod h1:ylS4c28ACSI59oJrOdW4pHS4n0Hw4TgSPHn8rpHl4Yw=
connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k=
github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536 h1:vhpjulzH5Tr4S3uJ3Y/9pNL481kPq5ERj13ceAW0/uE=
github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 h1:X8MJ0fnN5FPdcGF5Ij2/OW+HgiJrRg3AfHAx1PJtIzM=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/bufbuild/protovalidate-go v0.6.3 h1:wxQyzW035zM16Binbaz/nWAzS12dRIXhZdSUWRY7Fv0=
github.com/bufbuild/protovalidate-go v0.6.3/go.mod h1:J4PtwP9Z2YAGgB0+o+tTWEDtLtXvz/gfhFZD8pbzM/U=
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/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jaswdr/faker/v2 v2.3.0 h1:jgQ9UmU2Eb5tSQ8JkUS4tPoyTM2OtThQpOpwk7Fa9RY=
github.com/jaswdr/faker/v2 v2.3.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
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/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/stephenafamo/bob v0.28.1 h1:yQaHuhP9HoCldoRIrhB7SwcHKMCCSw5499h7ETFcBLs=
github.com/stephenafamo/bob v0.28.1/go.mod h1:S/D3dAbBZjBOcts9iv4ywsJDApFK86s3bUBulCoT6kE=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3Vmw1IakoaXocHmMIGgJFYob0vuK+CFWiJHQvz0jQ=
github.com/stephenafamo/scan v0.4.2 h1:mPmOjV84VCQdlYPPX1jZZHt5LYRHAPdEe/yuSaMi92Q=
github.com/stephenafamo/scan v0.4.2/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
github.com/stephenafamo/sqlparser v0.0.0-20230326220333-c2adaf4c30e8 h1:HR6pFkWHsRRVm+VUSjltpaI56XFl2hdwxdGw7hHIPDk=
github.com/stephenafamo/sqlparser v0.0.0-20230326220333-c2adaf4c30e8/go.mod h1:hOw+0TLAWETjE8UJG+q5bdj7EArbIombNBnEUoF7XwM=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ=
github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=

3
go/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
models/
*.db
gen/

18
go/api/api.go Normal file
View file

@ -0,0 +1,18 @@
package api
import (
"sync"
"github.com/stephenafamo/bob"
)
type API struct {
mu sync.Mutex
db bob.Executor
}
func (api *API) lockf(f func()) {
api.mu.Lock()
defer api.mu.Unlock()
f()
}

47
go/api/devices_create.go Normal file
View file

@ -0,0 +1,47 @@
package api
import (
"context"
"errors"
"time"
"connectrpc.com/connect"
"github.com/aarondl/opt/omit"
"github.com/mattn/go-sqlite3"
"github.com/tigorlazuardi/bluemage/go/gen/models"
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
)
func (api *API) DevicesCreate(ctx context.Context, params *models.Device) (device *models.Device, err error) {
now := time.Now()
api.lockf(func() {
device, err = models.Devices.Insert(ctx, api.db, &models.DeviceSetter{
Slug: omit.From(params.Slug),
Name: omit.From(params.Name),
ResolutionX: omit.From(params.ResolutionX),
ResolutionY: omit.From(params.ResolutionY),
AspectRatioTolerance: omit.From(params.AspectRatioTolerance),
MinX: omit.From(params.MinX),
MinY: omit.From(params.MinY),
MaxX: omit.From(params.MaxX),
MaxY: omit.From(params.MaxY),
NSFW: omit.From(params.NSFW),
SingleFolderMode: omit.From(params.SingleFolderMode),
Disabled: omit.From(params.Disabled),
CreatedAt: omit.From(now.Unix()),
UpdatedAt: omit.From(now.Unix()),
})
})
if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) {
if sqliteErr.Code == sqlite3.ErrConstraint {
return nil, errs.
Wrapw(sqliteErr, "device already exists", "params", params).
Code(connect.CodeAlreadyExists)
}
}
return nil, errs.Wrapw(err, "failed to create device", "params", params)
}
return device, nil
}

View file

@ -0,0 +1,22 @@
package api
import (
"context"
"connectrpc.com/connect"
"github.com/tigorlazuardi/bluemage/go/gen/models"
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
)
func (api *API) GetDevice(ctx context.Context, slug string) (device *models.Device, err error) {
device, err = models.FindDevice(ctx, api.db, slug)
if err != nil {
if err.Error() == "sql: no rows in result set" {
return device, errs.Wrapw(err, "device not found", "device", device).Code(connect.CodeNotFound)
}
return device, errs.Wrapw(err, "failed to find device", "device", device)
}
return device, nil
}

2
go/bobgen.yaml Normal file
View file

@ -0,0 +1,2 @@
sqlite:
dsn: ../data.db

View file

@ -1,3 +1,7 @@
package main
func main() {}
import "time"
func main() {
time.Now().Unix()
}

31
go/converter/converter.go Normal file
View file

@ -0,0 +1,31 @@
package converter
import (
"github.com/tigorlazuardi/bluemage/go/gen/models"
device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"
)
// goverter:converter
// goverter:extend BoolToInt8
// goverter:extend Int8ToBool
type DeviceConverter interface {
// goverter:ignore CreatedAt UpdatedAt
// goverter:map Nsfw NSFW
CreateDeviceRequestToModelsDevice(*device.CreateDeviceRequest) *models.Device
// goverter:ignore state sizeCache unknownFields
ModelsDeviceToCreateDeviceResponse(*models.Device) *device.CreateDeviceResponse
// goverter:ignore state sizeCache unknownFields
// goverter:map NSFW Nsfw
ModelsDeviceToGetDeviceResponse(*models.Device) *device.GetDeviceResponse
}
func BoolToInt8(b bool) int8 {
if b {
return 1
}
return 0
}
func Int8ToBool(i int8) bool {
return i > 0
}

84
go/pkg/caller/caller.go Normal file
View file

@ -0,0 +1,84 @@
package caller
import (
"context"
"log/slog"
"os"
"runtime"
"strings"
)
type Caller struct {
PC uintptr
Frame runtime.Frame
}
func (ca Caller) File() string {
return ca.Frame.File
}
func (ca Caller) ShortFile() string {
wd, err := os.Getwd()
if err != nil {
return ca.Frame.File
}
if after, found := strings.CutPrefix(ca.Frame.File, wd); found {
return strings.TrimPrefix(after, string(os.PathSeparator))
}
return ca.Frame.File
}
func (ca Caller) Line() int {
return ca.Frame.Line
}
func (ca Caller) Function() string {
return ca.Frame.Function
}
func (ca Caller) ShortFunction() string {
split := strings.Split(ca.Frame.Function, string(os.PathSeparator))
return split[len(split)-1]
}
func (ca Caller) LogValue() slog.Value {
if ca.PC == 0 {
return slog.AnyValue(nil)
}
return slog.GroupValue(
slog.String("file", ca.ShortFile()),
slog.Int("line", ca.Line()),
slog.String("function", ca.ShortFunction()),
)
}
func New(skip int) Caller {
var c Caller
pcs := make([]uintptr, 1)
n := runtime.Callers(skip, pcs)
if n == 0 {
return c
}
c.PC = pcs[0]
c.Frame, _ = runtime.CallersFrames(pcs).Next()
return c
}
func From(pc uintptr) Caller {
var c Caller
c.PC = pc
c.Frame, _ = runtime.CallersFrames([]uintptr{pc}).Next()
return c
}
type contextKey struct{}
func WithContext(ctx context.Context, caller Caller) context.Context {
return context.WithValue(ctx, contextKey{}, caller)
}
func FromContext(ctx context.Context) (Caller, bool) {
call, ok := ctx.Value(contextKey{}).(Caller)
return call, ok
}

231
go/pkg/errs/errs.go Normal file
View file

@ -0,0 +1,231 @@
package errs
import (
"context"
"errors"
"fmt"
"log/slog"
"reflect"
"strings"
"connectrpc.com/connect"
"github.com/tigorlazuardi/bluemage/go/pkg/caller"
)
type Error interface {
error
Message(msg string, args ...any) Error
GetMessage() string
Code(status connect.Code) Error
GetCode() connect.Code
Caller(pc caller.Caller) Error
GetCaller() caller.Caller
Details(...any) Error
GetDetails() []any
Log(ctx context.Context) Error
}
var (
_ Error = (*Err)(nil)
_ slog.LogValuer = (*Err)(nil)
)
type Err struct {
message string
code connect.Code
caller caller.Caller
details []any
origin error
}
// Unwrap implements the implementation of errors.Unwrap.
func (er *Err) Unwrap() error {
return er.origin
}
// LogValue implements slog.LogValuer.
func (er *Err) LogValue() slog.Value {
values := make([]slog.Attr, 0, 5)
if er.message != "" {
values = append(values, slog.String("message", er.message))
}
if er.code != 0 && er.code != connect.CodeInternal {
values = append(values, slog.String("code", er.code.String()))
}
if er.caller.PC != 0 {
values = append(values, slog.Attr{Key: "caller", Value: er.caller.LogValue()})
}
if len(er.details) > 0 {
values = append(values, slog.Group("details", er.details...))
}
if er.origin == nil {
values = append(values, slog.Any("error", er.origin))
} else if lv, ok := er.origin.(slog.LogValuer); ok {
values = append(values, slog.Attr{Key: "error", Value: lv.LogValue()})
} else {
values = append(values, slog.Attr{Key: "error", Value: slog.GroupValue(
slog.String("type", reflect.TypeOf(er.origin).String()),
slog.String("message", er.origin.Error()),
slog.Any("data", er.origin),
)})
}
return slog.GroupValue(values...)
}
// Caller implements Error.
func (e *Err) Caller(pc caller.Caller) Error {
e.caller = pc
return e
}
// Code implements Error.
func (e *Err) Code(status connect.Code) Error {
e.code = status
return e
}
// Details implements Error.
func (e *Err) Details(keysAndValues ...any) Error {
e.details = keysAndValues
return e
}
// Error implements Error.
//
// The error message is constructed by concatenating the message of the error,
// discarding any empty and duplicate messages in the chain.
//
// The error messages are concatenated with a colon and a space.
func (e *Err) Error() string {
s := strings.Builder{}
if e.message != "" {
s.WriteString(e.message)
if e.origin != nil {
s.WriteString(": ")
}
}
unwrap := errors.Unwrap(e)
for unwrap != nil {
var current string
if e, ok := unwrap.(Error); ok && e.GetMessage() != "" {
current = e.GetMessage()
} else {
current = unwrap.Error()
}
next := errors.Unwrap(unwrap)
if next != nil {
current, _ = strings.CutSuffix(current, next.Error())
current, _ = strings.CutSuffix(current, ": ")
}
if current != "" {
s.WriteString(current)
if next != nil {
s.WriteString(": ")
}
}
unwrap = next
}
return s.String()
}
// GetCaller implements Error.
func (e *Err) GetCaller() caller.Caller {
return e.caller
}
// GetCode implements Error.
func (e *Err) GetCode() connect.Code {
return e.code
}
// GetDetails implements Error.
func (e *Err) GetDetails() []any {
return e.details
}
// GetMessage implements Error.
func (e *Err) GetMessage() string {
return e.message
}
// Log implements Error.
func (e *Err) Log(ctx context.Context) Error {
panic("unimplemented")
}
// Message implements Error.
func (e *Err) Message(msg string, args ...any) Error {
e.message = fmt.Sprintf(msg, args...)
return e
}
func Wrap(err error, message string) Error {
return &Err{
origin: err,
caller: caller.New(3),
message: message,
code: connect.CodeInternal,
}
}
func Wrapw(err error, message string, details ...any) Error {
return &Err{
origin: err,
details: details,
message: message,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Wrapf(err error, message string, args ...any) Error {
message = fmt.Sprintf(message, args...)
return &Err{
origin: err,
message: message,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Fail(message string, details ...any) Error {
return &Err{
origin: errors.New(message),
details: details,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Failf(message string, args ...any) Error {
return &Err{
origin: fmt.Errorf(message, args...),
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func IntoConnectError(err error) error {
if err == nil {
return nil
}
if e := FindError(err); e != nil {
return connect.NewError(e.GetCode(), e)
}
return connect.NewError(connect.CodeUnknown, err)
}
func FindError(err error) Error {
for {
if e, ok := err.(Error); ok {
return e
}
if e, ok := err.(interface{ Unwrap() error }); ok {
err = e.Unwrap()
continue
}
return nil
}
}

View file

@ -0,0 +1,57 @@
package server
import (
"context"
"connectrpc.com/connect"
"github.com/bufbuild/protovalidate-go"
"github.com/tigorlazuardi/bluemage/go/api"
"github.com/tigorlazuardi/bluemage/go/gen/converter"
device "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1"
"github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect"
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
)
var _ v1connect.DeviceServiceHandler = (*DeviceHandler)(nil)
type DeviceHandler struct {
API *api.API
v1connect.UnimplementedDeviceServiceHandler
}
var (
convert converter.DeviceConverterImpl
validate, _ = protovalidate.New()
)
// CreateDevice implements v1connect.DeviceServiceHandler.
func (d *DeviceHandler) CreateDevice(ctx context.Context, request *connect.Request[device.CreateDeviceRequest]) (*connect.Response[device.CreateDeviceResponse], error) {
err := validate.Validate(request.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
dev := convert.CreateDeviceRequestToModelsDevice(request.Msg)
dev, err = d.API.DevicesCreate(ctx, dev)
if err != nil {
return nil, errs.IntoConnectError(err)
}
devResp := convert.ModelsDeviceToCreateDeviceResponse(dev)
return connect.NewResponse(devResp), nil
}
// GetDevice implements v1connect.DeviceServiceHandler.
func (d *DeviceHandler) GetDevice(ctx context.Context, request *connect.Request[device.GetDeviceRequest]) (*connect.Response[device.GetDeviceResponse], error) {
err := validate.Validate(request.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
dev, err := d.API.GetDevice(ctx, request.Msg.Slug)
if err != nil {
return nil, errs.IntoConnectError(err)
}
devResp := convert.ModelsDeviceToGetDeviceResponse(dev)
return connect.NewResponse(devResp), nil
}

5
go/server/server.go Normal file
View file

@ -0,0 +1,5 @@
package server
type Server struct {
DeviceHandler
}

12
go/tools/tools.go Normal file
View file

@ -0,0 +1,12 @@
package tools
// These blank imports are used to ensure that dependency tools are included in the go.mod file.
import (
_ "connectrpc.com/connect"
_ "github.com/aarondl/opt"
_ "github.com/bufbuild/protovalidate-go"
_ "github.com/stephenafamo/bob"
_ "google.golang.org/protobuf/reflect/protoreflect"
_ "google.golang.org/protobuf/runtime/protoimpl"
)

View file

@ -3,15 +3,15 @@
CREATE TABLE metrics (
name VARCHAR(255) NOT NULL PRIMARY KEY COLLATE NOCASE,
value BIGINT DEFAULT 0 NOT NULL,
created_at BIGINT DEFAULT 0 NOT NULL,
updated_at BIGINT DEFAULT 0 NOT NULL
created_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL
);
CREATE UNIQUE INDEX idx_metrics_name ON metrics(name);
CREATE TRIGGER update_metrics_timestamp_after_update AFTER UPDATE ON metrics FOR EACH ROW
BEGIN
UPDATE metrics SET updated_at = CURRENT_TIMESTAMP WHERE name = old.name;
UPDATE metrics SET updated_at = (strftime('%s', 'now')) WHERE name = old.name;
END;
-- +goose StatementEnd

View file

@ -2,26 +2,26 @@
-- +goose StatementBegin
CREATE TABLE subreddits (
name VARCHAR(30) NOT NULL PRIMARY KEY COLLATE NOCASE,
disable_scheduler INT NOT NULL DEFAULT 0,
disable_scheduler TINYINT NOT NULL DEFAULT 0,
"type" VARCHAR(5) NOT NULL DEFAULT 'r',
schedule VARCHAR(20) NOT NULL DEFAULT '@daily',
countback INT NOT NULL DEFAULT 300,
image_cover VARCHAR(255) NOT NULL DEFAULT '',
created_at BIGINT DEFAULT 0 NOT NULL,
updated_at BIGINT DEFAULT 0 NOT NULL
created_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at BIGINT DEFAULT (strftime('%s', 'now')) NOT NULL
);
CREATE UNIQUE INDEX idx_subreddits_name ON subreddits(name);
CREATE TRIGGER subreddits_update_timestamp AFTER UPDATE ON subreddits FOR EACH ROW
BEGIN
UPDATE subreddits SET updated_at = CURRENT_TIMESTAMP WHERE name = old.name;
UPDATE subreddits SET updated_at = (strftime('%s', 'now')) WHERE name = old.name;
END;
CREATE TRIGGER subreddits_insert_create_total_image_metrics AFTER INSERT on subreddits FOR EACH ROW
BEGIN
INSERT INTO metrics (name, value, created_at, updated_at)
VALUES (CONCAT('subreddits.', NEW.name, '.total_images'), 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO metrics (name, value)
VALUES (CONCAT('subreddits.', NEW.name, '.total_images'), 0);
END;
CREATE TRIGGER subreddits_delete_total_image_metrics AFTER DELETE on subreddits FOR EACH ROW

View file

@ -0,0 +1,42 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE devices (
slug VARCHAR(255) PRIMARY KEY COLLATE NOCASE,
disabled TINYINT NOT NULL DEFAULT 0,
name TEXT NOT NULL COLLATE NOCASE,
resolution_x DOUBLE NOT NULL,
resolution_y DOUBLE NOT NULL,
aspect_ratio_tolerance DOUBLE NOT NULL DEFAULT 0.2,
min_x INTEGER NOT NULL DEFAULT 0,
min_y INTEGER NOT NULL DEFAULT 0,
max_x INTEGER NOT NULL DEFAULT 0,
max_y INTEGER NOT NULL DEFAULT 0,
nsfw TINYINT NOT NULL DEFAULT 0,
single_folder_mode TINYINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE UNIQUE INDEX idx_devices_unique_slug ON devices(slug);
CREATE TRIGGER devices_update_timestamp AFTER UPDATE ON devices FOR EACH ROW
BEGIN
UPDATE devices SET updated_at = strftime('%s', 'now') WHERE slug = old.slug;
END;
CREATE TRIGGER devices_insert_create_total_image_metrics AFTER INSERT on devices FOR EACH ROW
BEGIN
INSERT INTO metrics (name, value)
VALUES (CONCAT('devices.', NEW.slug, '.total_images'), 0);
END;
CREATE TRIGGER devices_delete_total_image_metrics AFTER DELETE on devices FOR EACH ROW
BEGIN
DELETE FROM metrics WHERE name = CONCAT('devices.', OLD.slug, '.total_images');
END;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE devices;
-- +goose StatementEnd

View file

@ -7,26 +7,55 @@ import "buf/validate/validate.proto";
option go_package = "github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1";
service DeviceService {
// GetDevice fetches a device by its slug.
//
// If the device is not found, an error will be returned.
rpc GetDevice(GetDeviceRequest) returns (GetDeviceResponse) {}
// CreateDevice creates a new device.
rpc CreateDevice(CreateDeviceRequest) returns (CreateDeviceResponse) {}
}
message GetDeviceRequest {
// The `slug` is a unique identifier for the device.
string slug = 1 [(buf.validate.field).string.min_len = 1];
}
message GetDeviceResponse {
string slug = 1;
bool disabled = 2;
string name = 3;
double resolution_x = 4;
double resolution_y = 5;
double aspect_ratio_tolerance = 6;
int32 min_x = 7;
int32 min_y = 8;
int32 max_x = 9;
int32 max_y = 10;
bool nsfw = 11;
bool single_folder_mode = 12;
int64 created_at = 13;
int64 updated_at = 14;
}
message CreateDeviceRequest {
// The `slug` is a unique identifier for the device, used to identify the device within the system.
// Each `slug` must be unique across all devices.
string slug = 1 [(buf.validate.field).string.min_len = 1];
// `enable` is a flag to set if a device is enabled upon creation. Default false.
bool enable = 2;
// `disabled` is a flag to set if a device is enabled upon creation. Default false.
bool disabled = 2;
// The name of the device. This is used for display purposes.
string name = 3;
// The display x resolution of the device.
double resolution_x = 4;
// The value must be greater than 0.
double resolution_x = 4 [(buf.validate.field).double.gt = 0];
// The display y resolution of the device.
double resolution_y = 5;
// The value must be greater than 0.
double resolution_y = 5 [(buf.validate.field).double.gt = 0];
// aspect_ratio_tolerance is a tolerance for aspect ratio images to be accepted.
//
@ -43,7 +72,6 @@ message CreateDeviceRequest {
// accepted by the device even though it is 2 times bigger than the device resolution.
//
// If you want to filter image size, use min_x, min_y, max_x, max_y fields.
//
double aspect_ratio_tolerance = 6;
// The minimum x resolution required for an image to be accepted by the device.
@ -51,35 +79,35 @@ message CreateDeviceRequest {
// A value of 0 indicates no minimum x resolution.
//
// The default value is 0.
int32 min_x = 7;
int32 min_x = 7 [(buf.validate.field).int32.gte = 0];
// The minimum y resolution required for an image to be accepted by the device.
//
// A value of 0 indicates no minimum y resolution.
//
// The default value is 0.
int32 min_y = 8;
int32 min_y = 8 [(buf.validate.field).int32.gte = 0];
// The maximum x resolution allowed for an image to be accepted by the device.
//
// A value of 0 indicates no maximum x resolution.
//
// The default value is 0.
int32 max_x = 9;
int32 max_x = 9 [(buf.validate.field).int32.gte = 0];
// The maximum y resolution allowed for an image to be accepted by the device.
//
// A value of 0 indicates no maximum y resolution.
//
// The default value is 0.
int32 max_y = 10;
int32 max_y = 10 [(buf.validate.field).int32.gte = 0];
// The `nsfw` parameter allows the device to accept NSFW (Not Safe For Work) images when set to true.
bool nsfw = 11;
// `windows_wallpaper_mode` when set to true, downloaded images will be put on single folder location
// `single_folder_mode` when set to true, downloaded images will be put on single folder location
// instead of inside folders separated by subreddit names.
bool windows_wallpaper_mode = 12;
bool single_folder_mode = 12;
}
message CreateDeviceResponse {

1
web/.gitignore vendored
View file

@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
gen/