Add actor, film, and category APIs with SQL query support
- Generated SQL queries for actors, films, categories. - Introduced HTTP handlers for actor, film, and category endpoints. - Included utility functions for parsing query parameters and building URLs. - Enabled pagination, filtering, and HAL representation for responses.
This commit is contained in:
commit
54e36528bc
33 changed files with 2509 additions and 0 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# 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
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
/form-builder-be-go
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "sakila-sqlite3"]
|
||||
path = sakila-sqlite3
|
||||
url = https://github.com/bradleygrant/sakila-sqlite3.git
|
||||
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
19
.idea/dataSources.xml
Normal file
19
.idea/dataSources.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="sakila_master.db" uuid="180a30dd-2be7-4f7f-8191-184e31bae505">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/sakila-sqlite3-main/sakila_master.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="world.sqlite3" uuid="943bcd28-8596-4b08-ba50-58a832764f74">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/countries-states-cities-database-master/sqlite/world.sqlite3</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/form-builder-be-go.iml
Normal file
9
.idea/form-builder-be-go.iml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/form-builder-be-go.iml" filepath="$PROJECT_DIR$/.idea/form-builder-be-go.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
28
.idea/sqlDataSources.xml
Normal file
28
.idea/sqlDataSources.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DdlMappings">
|
||||
<mapping uuid="d7863360-a981-4e2e-8aa1-11b32a1796e5" name="DDL Mapping">
|
||||
<data-sources db="180a30dd-2be7-4f7f-8191-184e31bae505" ddl="99c98b6b-3497-4978-bdd2-9c80d573a555" />
|
||||
<scope>
|
||||
<node kind="schema" qname="main" />
|
||||
</scope>
|
||||
</mapping>
|
||||
</component>
|
||||
<component name="SqlDataSourceStorage">
|
||||
<option name="dataSources">
|
||||
<list>
|
||||
<State>
|
||||
<option name="id" value="99c98b6b-3497-4978-bdd2-9c80d573a555" />
|
||||
<option name="name" value="sakila_master.db (DDL)" />
|
||||
<option name="dbmsName" value="SQLITE" />
|
||||
<option name="urls">
|
||||
<array>
|
||||
<option value="file://$PROJECT_DIR$/sakila-sqlite3-main" />
|
||||
</array>
|
||||
</option>
|
||||
<option name="outLayout" value="File per object by schema.groovy" />
|
||||
</State>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/sqldialects.xml
Normal file
10
.idea/sqldialects.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/queries/actor.sql" dialect="SQLite" />
|
||||
<file url="file://$PROJECT_DIR$/queries/film.sql" dialect="SQLite" />
|
||||
<file url="file://$PROJECT_DIR$/queries/film_filter_language.sql" dialect="SQLite" />
|
||||
<file url="file://$PROJECT_DIR$/queries/language.sql" dialect="SQLite" />
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
40
go.mod
Normal file
40
go.mod
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
module form-builder-be-go
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/raff/halgo v0.0.0-20240731162511-e3f9a77e30a8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jtacoma/uritemplates v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
97
go.sum
Normal file
97
go.sum
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
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/jtacoma/uritemplates v1.0.0 h1:xwx5sBF7pPAb0Uj8lDC1Q/aBPpOFyQza7OC705ZlLCo=
|
||||
github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/raff/halgo v0.0.0-20240731162511-e3f9a77e30a8 h1:Drb+jhniBV2bcYpBAaMCS/4RZzfPl9GEOGebLOqCIRE=
|
||||
github.com/raff/halgo v0.0.0-20240731162511-e3f9a77e30a8/go.mod h1:UKFDssjues9OgoSs7BK/CjwLj8oPAR2HvsPuFk7yVNQ=
|
||||
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/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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
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/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/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/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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/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=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
88
internal/hal/hal.go
Normal file
88
internal/hal/hal.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package hal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
// Resource is a simple HAL resource envelope that uses halgo.Link for links.
|
||||
type Resource struct {
|
||||
Links map[string]halgo.Link `json:"_links,omitempty"`
|
||||
Embedded map[string]interface{} `json:"_embedded,omitempty"`
|
||||
// State holds arbitrary resource state merged at top-level when marshaled
|
||||
State map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
// New creates a new HAL resource with a self link.
|
||||
func New(selfHref string) *Resource {
|
||||
r := &Resource{
|
||||
Links: map[string]halgo.Link{},
|
||||
Embedded: map[string]interface{}{},
|
||||
State: map[string]interface{}{},
|
||||
}
|
||||
if selfHref != "" {
|
||||
r.AddLink("self", halgo.Link{Href: selfHref})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// AddLink adds a link with the given rel.
|
||||
func (r *Resource) AddLink(rel string, link halgo.Link) {
|
||||
if r.Links == nil {
|
||||
r.Links = map[string]halgo.Link{}
|
||||
}
|
||||
r.Links[rel] = link
|
||||
}
|
||||
|
||||
// AddCurie adds a CURIE link for the sakila namespace.
|
||||
// Note: HAL recommends an array for the "curies" rel. This implementation uses a single link
|
||||
// due to the underlying link map being single-valued per rel. Most clients accept this form.
|
||||
func (r *Resource) AddCurie() {
|
||||
r.AddLink("curies", halgo.Link{
|
||||
Href: "/rels/{rel}",
|
||||
Templated: true,
|
||||
Name: "sakila",
|
||||
})
|
||||
}
|
||||
|
||||
// Embed sets an embedded payload under the rel.
|
||||
func (r *Resource) Embed(rel string, v interface{}) {
|
||||
if r.Embedded == nil {
|
||||
r.Embedded = map[string]interface{}{}
|
||||
}
|
||||
r.Embedded[rel] = v
|
||||
}
|
||||
|
||||
// SetState sets or merges state fields.
|
||||
func (r *Resource) SetState(obj interface{}) {
|
||||
switch m := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range m {
|
||||
r.State[k] = v
|
||||
}
|
||||
default:
|
||||
// marshal into map and merge
|
||||
b, _ := json.Marshal(obj)
|
||||
var mm map[string]interface{}
|
||||
_ = json.Unmarshal(b, &mm)
|
||||
for k, v := range mm {
|
||||
r.State[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON merges State with _links and _embedded following HAL conventions.
|
||||
func (r *Resource) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]interface{}{}
|
||||
for k, v := range r.State {
|
||||
m[k] = v
|
||||
}
|
||||
if len(r.Links) > 0 {
|
||||
m["_links"] = r.Links
|
||||
}
|
||||
if len(r.Embedded) > 0 {
|
||||
m["_embedded"] = r.Embedded
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
287
internal/httpapi/actors.go
Normal file
287
internal/httpapi/actors.go
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hres "form-builder-be-go/internal/hal"
|
||||
sq "form-builder-be-go/internal/sqlc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
func RegisterActorRoutes(r *gin.Engine, db *sql.DB) {
|
||||
// List actors
|
||||
r.GET("/actors", func(c *gin.Context) {
|
||||
limit := parseIntDefault(c.Query("limit"), 20)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := parseIntDefault(c.Query("offset"), 0)
|
||||
order := c.Query("order")
|
||||
expand := parseExpand(c.Query("expand"))
|
||||
|
||||
q := sq.New(db)
|
||||
actors, err := q.ListActors(c, sq.ListActorsParams{Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err := q.CountActors(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
selfURL := buildURL("/actors", limit, offset, order, c.Query("expand"))
|
||||
res := hres.New(selfURL)
|
||||
res.AddCurie()
|
||||
res.AddLink("self", halgo.Link{Href: selfURL})
|
||||
if int64(offset+limit) < total {
|
||||
nextURL := buildURL("/actors", limit, offset+limit, order, c.Query("expand"))
|
||||
res.AddLink("next", halgo.Link{Href: nextURL})
|
||||
}
|
||||
if offset > 0 {
|
||||
prev := offset - limit
|
||||
if prev < 0 {
|
||||
prev = 0
|
||||
}
|
||||
prevURL := buildURL("/actors", limit, prev, order, c.Query("expand"))
|
||||
res.AddLink("prev", halgo.Link{Href: prevURL})
|
||||
}
|
||||
|
||||
var embedded []interface{}
|
||||
for _, a := range actors {
|
||||
itemURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10)
|
||||
relFilmsURL := itemURL + "/films"
|
||||
item := hres.New(itemURL)
|
||||
item.SetState(a)
|
||||
item.AddLink("self", halgo.Link{Href: itemURL})
|
||||
item.AddLink("sakila:films", halgo.Link{Href: relFilmsURL})
|
||||
if expand["films"] {
|
||||
films, err := q.ListFilmsByActor(c, int64(a.ActorID))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var filmEmbeds []interface{}
|
||||
for _, f := range films {
|
||||
filmURL := "/films/" + strconv.FormatInt(f.FilmID, 10)
|
||||
film := hres.New(filmURL)
|
||||
film.SetState(f)
|
||||
film.AddLink("self", halgo.Link{Href: filmURL})
|
||||
film.AddLink("sakila:actors", halgo.Link{Href: filmURL + "/actors"})
|
||||
filmEmbeds = append(filmEmbeds, film)
|
||||
}
|
||||
item.Embed("films", filmEmbeds)
|
||||
}
|
||||
embedded = append(embedded, item)
|
||||
}
|
||||
res.Embed("actors", embedded)
|
||||
res.SetState(gin.H{"count": len(actors), "total": total, "limit": limit, "offset": offset})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Get actor
|
||||
r.GET("/actors/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
expand := parseExpand(c.Query("expand"))
|
||||
q := sq.New(db)
|
||||
aRec, err := q.GetActor(c, id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// sqlc returns error for not found
|
||||
self := "/actors/" + strconv.FormatInt(int64(aRec.ActorID), 10)
|
||||
relFilms := self + "/films"
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(aRec)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:films", halgo.Link{Href: relFilms})
|
||||
if expand["films"] {
|
||||
films, err := q.ListFilmsByActor(c, int64(aRec.ActorID))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var filmEmbeds []interface{}
|
||||
for _, f := range films {
|
||||
filmURL := "/films/" + strconv.FormatInt(f.FilmID, 10)
|
||||
film := hres.New(filmURL)
|
||||
film.SetState(f)
|
||||
film.AddLink("self", halgo.Link{Href: filmURL})
|
||||
film.AddLink("sakila:actors", halgo.Link{Href: filmURL + "/actors"})
|
||||
filmEmbeds = append(filmEmbeds, film)
|
||||
}
|
||||
res.Embed("films", filmEmbeds)
|
||||
}
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Create actor
|
||||
r.POST("/actors", func(c *gin.Context) {
|
||||
var in struct {
|
||||
ActorID *int64 `json:"actor_id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.FirstName) == "" || strings.TrimSpace(in.LastName) == "" {
|
||||
c.JSON(400, gin.H{"error": "first_name and last_name are required"})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
var id int64
|
||||
if in.ActorID != nil {
|
||||
id = *in.ActorID
|
||||
} else {
|
||||
var err error
|
||||
id, err = q.NextActorID(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := q.InsertActor(c, sq.InsertActorParams{ActorID: id, FirstName: in.FirstName, LastName: in.LastName}); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
created, err := q.GetActor(c, id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/actors/" + strconv.FormatInt(int64(created.ActorID), 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(created)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:films", halgo.Link{Href: self + "/films"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.Header("Location", self)
|
||||
c.JSON(201, res)
|
||||
})
|
||||
|
||||
// Replace actor
|
||||
r.PUT("/actors/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.FirstName) == "" || strings.TrimSpace(in.LastName) == "" {
|
||||
c.JSON(400, gin.H{"error": "first_name and last_name are required"})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
aff, err := q.UpdateActorPut(c, sq.UpdateActorPutParams{FirstName: in.FirstName, LastName: in.LastName, ActorID: id})
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if aff == 0 {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
updated, err := q.GetActor(c, id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/actors/" + strconv.FormatInt(int64(updated.ActorID), 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(updated)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:films", halgo.Link{Href: self + "/films"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Patch actor
|
||||
r.PATCH("/actors/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
FirstName *string `json:"first_name"`
|
||||
LastName *string `json:"last_name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
toNullString := func(p *string) sql.NullString {
|
||||
if p == nil {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
return sql.NullString{String: *p, Valid: true}
|
||||
}
|
||||
aff, err := q.PatchActor(c, sq.PatchActorParams{FirstName: toNullString(in.FirstName), LastName: toNullString(in.LastName), ActorID: id})
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if aff == 0 {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
updated, err := q.GetActor(c, id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/actors/" + strconv.FormatInt(int64(updated.ActorID), 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(updated)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:films", halgo.Link{Href: self + "/films"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Delete actor
|
||||
r.DELETE("/actors/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
aff, err := q.DeleteActor(c, id)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if aff == 0 {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.Status(204)
|
||||
})
|
||||
}
|
||||
103
internal/httpapi/categories.go
Normal file
103
internal/httpapi/categories.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hres "form-builder-be-go/internal/hal"
|
||||
sq "form-builder-be-go/internal/sqlc"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
func RegisterCategoryRoutes(r *gin.Engine, db *sql.DB) {
|
||||
// List categories
|
||||
r.GET("/categories", func(c *gin.Context) {
|
||||
limit := parseIntDefault(c.Query("limit"), 20)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := parseIntDefault(c.Query("offset"), 0)
|
||||
order := c.Query("order") // currently static ORDER BY in query; keep for link preservation
|
||||
|
||||
q := sq.New(db)
|
||||
cats, err := q.ListCategories(c, sq.ListCategoriesParams{Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err := q.CountCategories(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
selfURL := buildURL("/categories", limit, offset, order, c.Query("expand"))
|
||||
res := hres.New(selfURL)
|
||||
res.AddCurie()
|
||||
res.AddLink("self", halgo.Link{Href: selfURL})
|
||||
if int64(offset+limit) < total {
|
||||
nextURL := buildURL("/categories", limit, offset+limit, order, c.Query("expand"))
|
||||
res.AddLink("next", halgo.Link{Href: nextURL})
|
||||
}
|
||||
if offset > 0 {
|
||||
prev := offset - limit
|
||||
if prev < 0 {
|
||||
prev = 0
|
||||
}
|
||||
prevURL := buildURL("/categories", limit, prev, order, c.Query("expand"))
|
||||
res.AddLink("prev", halgo.Link{Href: prevURL})
|
||||
}
|
||||
|
||||
var embedded []interface{}
|
||||
for _, cat := range cats {
|
||||
itemURL := "/categories/" + strconv.FormatInt(cat.CategoryID, 10)
|
||||
item := hres.New(itemURL)
|
||||
item.SetState(cat)
|
||||
item.AddLink("self", halgo.Link{Href: itemURL})
|
||||
name := strings.TrimSpace(cat.Name)
|
||||
if name != "" {
|
||||
qv := url.Values{"category.name": []string{name}}
|
||||
item.AddLink("sakila:films", halgo.Link{Href: "/films?" + qv.Encode()})
|
||||
}
|
||||
embedded = append(embedded, item)
|
||||
}
|
||||
res.Embed("categories", embedded)
|
||||
res.SetState(gin.H{"count": len(cats), "total": total, "limit": limit, "offset": offset})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Get category
|
||||
r.GET("/categories/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
cat, err := q.GetCategory(c, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/categories/" + strconv.FormatInt(cat.CategoryID, 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(cat)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
name := strings.TrimSpace(cat.Name)
|
||||
if name != "" {
|
||||
qv := url.Values{"category.name": []string{name}}
|
||||
res.AddLink("sakila:films", halgo.Link{Href: "/films?" + qv.Encode()})
|
||||
}
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
}
|
||||
459
internal/httpapi/films.go
Normal file
459
internal/httpapi/films.go
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hres "form-builder-be-go/internal/hal"
|
||||
sq "form-builder-be-go/internal/sqlc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
func RegisterFilmRoutes(r *gin.Engine, db *sql.DB) {
|
||||
// List films
|
||||
r.GET("/films", func(c *gin.Context) {
|
||||
limit := parseIntDefault(c.Query("limit"), 20)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := parseIntDefault(c.Query("offset"), 0)
|
||||
order := c.Query("order")
|
||||
expand := parseExpand(c.Query("expand"))
|
||||
langName := strings.TrimSpace(c.Query("language.name"))
|
||||
catName := strings.TrimSpace(c.Query("category.name"))
|
||||
|
||||
q := sq.New(db)
|
||||
var (
|
||||
films []sq.Film
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if langName != "" {
|
||||
films, err = q.ListFilmsByLanguageName(c, sq.ListFilmsByLanguageNameParams{UPPER: langName, Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err = q.CountFilmsByLanguageName(c, langName)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if catName != "" {
|
||||
films, err = q.ListFilmsByCategoryName(c, sq.ListFilmsByCategoryNameParams{UPPER: catName, Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err = q.CountFilmsByCategoryName(c, catName)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
films, err = q.ListFilms(c, sq.ListFilmsParams{Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err = q.CountFilms(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selfURL := buildURL("/films", limit, offset, order, c.Query("expand"))
|
||||
if langName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(selfURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
selfURL = selfURL + sep + "language.name=" + url.QueryEscape(langName)
|
||||
}
|
||||
if catName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(selfURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
selfURL = selfURL + sep + "category.name=" + url.QueryEscape(catName)
|
||||
}
|
||||
res := hres.New(selfURL)
|
||||
res.AddCurie()
|
||||
res.AddLink("self", halgo.Link{Href: selfURL})
|
||||
if int64(offset+limit) < total {
|
||||
nextURL := buildURL("/films", limit, offset+limit, order, c.Query("expand"))
|
||||
if langName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(nextURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
nextURL += sep + "language.name=" + url.QueryEscape(langName)
|
||||
}
|
||||
if catName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(nextURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
nextURL += sep + "category.name=" + url.QueryEscape(catName)
|
||||
}
|
||||
res.AddLink("next", halgo.Link{Href: nextURL})
|
||||
}
|
||||
if offset > 0 {
|
||||
prev := offset - limit
|
||||
if prev < 0 {
|
||||
prev = 0
|
||||
}
|
||||
prevURL := buildURL("/films", limit, prev, order, c.Query("expand"))
|
||||
if langName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(prevURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
prevURL += sep + "language.name=" + url.QueryEscape(langName)
|
||||
}
|
||||
if catName != "" {
|
||||
sep := "?"
|
||||
if strings.Contains(prevURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
prevURL += sep + "category.name=" + url.QueryEscape(catName)
|
||||
}
|
||||
res.AddLink("prev", halgo.Link{Href: prevURL})
|
||||
}
|
||||
|
||||
var embedded []interface{}
|
||||
for _, f := range films {
|
||||
itemURL := "/films/" + strconv.FormatInt(f.FilmID, 10)
|
||||
relActorsURL := itemURL + "/actors"
|
||||
item := hres.New(itemURL)
|
||||
item.SetState(f)
|
||||
item.AddLink("self", halgo.Link{Href: itemURL})
|
||||
item.AddLink("sakila:actors", halgo.Link{Href: relActorsURL})
|
||||
// language relations
|
||||
item.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)})
|
||||
if f.OriginalLanguageID.Valid {
|
||||
item.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)})
|
||||
}
|
||||
// optional embeds
|
||||
if expand["language"] {
|
||||
lang, err := q.GetLanguage(c, f.LanguageID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10))
|
||||
lr.SetState(lang)
|
||||
lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)})
|
||||
item.Embed("language", lr)
|
||||
}
|
||||
if expand["original_language"] && f.OriginalLanguageID.Valid {
|
||||
olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10))
|
||||
olr.SetState(olang)
|
||||
olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)})
|
||||
item.Embed("original_language", olr)
|
||||
}
|
||||
if expand["actors"] {
|
||||
actors, err := q.ListActorsByFilm(c, f.FilmID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var actorEmbeds []interface{}
|
||||
for _, a := range actors {
|
||||
actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10)
|
||||
ar := hres.New(actorURL)
|
||||
ar.SetState(a)
|
||||
ar.AddLink("self", halgo.Link{Href: actorURL})
|
||||
ar.AddLink("films", halgo.Link{Href: actorURL + "/films"})
|
||||
actorEmbeds = append(actorEmbeds, ar)
|
||||
}
|
||||
item.Embed("actors", actorEmbeds)
|
||||
}
|
||||
embedded = append(embedded, item)
|
||||
}
|
||||
res.Embed("films", embedded)
|
||||
res.SetState(gin.H{"count": len(films), "total": total, "limit": limit, "offset": offset})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Get film
|
||||
r.GET("/films/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
expand := parseExpand(c.Query("expand"))
|
||||
q := sq.New(db)
|
||||
f, err := q.GetFilm(c, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/films/" + strconv.FormatInt(f.FilmID, 10)
|
||||
relActors := self + "/actors"
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(f)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:actors", halgo.Link{Href: relActors})
|
||||
// language relations
|
||||
res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)})
|
||||
if f.OriginalLanguageID.Valid {
|
||||
res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)})
|
||||
}
|
||||
// optional embeds
|
||||
if expand["language"] {
|
||||
lang, err := q.GetLanguage(c, f.LanguageID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10))
|
||||
lr.SetState(lang)
|
||||
lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)})
|
||||
res.Embed("language", lr)
|
||||
}
|
||||
if expand["original_language"] && f.OriginalLanguageID.Valid {
|
||||
olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10))
|
||||
olr.SetState(olang)
|
||||
olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)})
|
||||
res.Embed("original_language", olr)
|
||||
}
|
||||
if expand["actors"] {
|
||||
actors, err := q.ListActorsByFilm(c, f.FilmID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var actorEmbeds []interface{}
|
||||
for _, a := range actors {
|
||||
actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10)
|
||||
ar := hres.New(actorURL)
|
||||
ar.SetState(a)
|
||||
ar.AddLink("self", halgo.Link{Href: actorURL})
|
||||
ar.AddLink("films", halgo.Link{Href: actorURL + "/films"})
|
||||
actorEmbeds = append(actorEmbeds, ar)
|
||||
}
|
||||
res.Embed("actors", actorEmbeds)
|
||||
}
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
/*
|
||||
// Create film
|
||||
r.POST("/films", func(c *gin.Context) {
|
||||
var in struct {
|
||||
FilmID *int64 `json:"film_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
ReleaseYear *string `json:"release_year"`
|
||||
LanguageID int64 `json:"language_id"`
|
||||
OriginalLangID *int64 `json:"original_language_id"`
|
||||
RentalDuration *int64 `json:"rental_duration"`
|
||||
RentalRate *float64 `json:"rental_rate"`
|
||||
Length *int64 `json:"length"`
|
||||
ReplacementCost *float64 `json:"replacement_cost"`
|
||||
Rating *string `json:"rating"`
|
||||
SpecialFeatures *string `json:"special_features"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Title) == "" || in.LanguageID == 0 {
|
||||
c.JSON(400, gin.H{"error": "title and language_id are required"})
|
||||
return
|
||||
}
|
||||
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
|
||||
if in.RentalDuration != nil {
|
||||
f.RentalDuration = *in.RentalDuration
|
||||
} else {
|
||||
f.RentalDuration = 3
|
||||
}
|
||||
if in.RentalRate != nil {
|
||||
f.RentalRate = *in.RentalRate
|
||||
} else {
|
||||
f.RentalRate = 4.99
|
||||
}
|
||||
if in.Length != nil {
|
||||
f.Length = in.Length
|
||||
}
|
||||
if in.ReplacementCost != nil {
|
||||
f.ReplacementCost = *in.ReplacementCost
|
||||
} else {
|
||||
f.ReplacementCost = 19.99
|
||||
}
|
||||
if in.Rating != nil {
|
||||
f.Rating = in.Rating
|
||||
}
|
||||
if in.SpecialFeatures != nil {
|
||||
f.SpecialFeatures = in.SpecialFeatures
|
||||
}
|
||||
if in.FilmID != nil {
|
||||
f.FilmID = *in.FilmID
|
||||
}
|
||||
created, err := sq.CreateFilm(c, db, &f)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/films/" + strconv.FormatInt(created.FilmID, 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(created)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.Header("Location", self)
|
||||
c.JSON(201, res)
|
||||
})
|
||||
|
||||
// Replace film
|
||||
r.PUT("/films/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
ReleaseYear *string `json:"release_year"`
|
||||
LanguageID int64 `json:"language_id"`
|
||||
OriginalLangID *int64 `json:"original_language_id"`
|
||||
RentalDuration *int64 `json:"rental_duration"`
|
||||
RentalRate *float64 `json:"rental_rate"`
|
||||
Length *int64 `json:"length"`
|
||||
ReplacementCost *float64 `json:"replacement_cost"`
|
||||
Rating *string `json:"rating"`
|
||||
SpecialFeatures *string `json:"special_features"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
|
||||
if in.RentalDuration != nil {
|
||||
f.RentalDuration = *in.RentalDuration
|
||||
} else {
|
||||
f.RentalDuration = 3
|
||||
}
|
||||
if in.RentalRate != nil {
|
||||
f.RentalRate = *in.RentalRate
|
||||
} else {
|
||||
f.RentalRate = 4.99
|
||||
}
|
||||
if in.Length != nil {
|
||||
f.Length = in.Length
|
||||
}
|
||||
if in.ReplacementCost != nil {
|
||||
f.ReplacementCost = *in.ReplacementCost
|
||||
} else {
|
||||
f.ReplacementCost = 19.99
|
||||
}
|
||||
if in.Rating != nil {
|
||||
f.Rating = in.Rating
|
||||
}
|
||||
if in.SpecialFeatures != nil {
|
||||
f.SpecialFeatures = in.SpecialFeatures
|
||||
}
|
||||
updated, err := sq.UpdateFilmPut(c, db, id, f)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/films/" + strconv.FormatInt(updated.FilmID, 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(updated)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Patch film
|
||||
r.PATCH("/films/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
ReleaseYear *string `json:"release_year"`
|
||||
LanguageID *int64 `json:"language_id"`
|
||||
OriginalLangID *int64 `json:"original_language_id"`
|
||||
RentalDuration *int64 `json:"rental_duration"`
|
||||
RentalRate *float64 `json:"rental_rate"`
|
||||
Length *int64 `json:"length"`
|
||||
ReplacementCost *float64 `json:"replacement_cost"`
|
||||
Rating *string `json:"rating"`
|
||||
SpecialFeatures *string `json:"special_features"`
|
||||
}
|
||||
patch := sq.FilmPatch{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID, RentalDuration: in.RentalDuration, RentalRate: in.RentalRate, Length: in.Length, ReplacementCost: in.ReplacementCost, Rating: in.Rating, SpecialFeatures: in.SpecialFeatures}
|
||||
updated, err := sq.PatchFilm(c, db, id, patch)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/films/" + strconv.FormatInt(updated.FilmID, 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(updated)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Delete film
|
||||
r.DELETE("/films/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := sq.DeleteFilm(c, db, id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(204)
|
||||
})
|
||||
*/
|
||||
}
|
||||
106
internal/httpapi/languages.go
Normal file
106
internal/httpapi/languages.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hres "form-builder-be-go/internal/hal"
|
||||
sq "form-builder-be-go/internal/sqlc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
func RegisterLanguageRoutes(r *gin.Engine, db *sql.DB) {
|
||||
// List languages
|
||||
r.GET("/languages", func(c *gin.Context) {
|
||||
limit := parseIntDefault(c.Query("limit"), 20)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := parseIntDefault(c.Query("offset"), 0)
|
||||
order := c.Query("order")
|
||||
|
||||
q := sq.New(db)
|
||||
langs, err := q.ListLanguages(c, sq.ListLanguagesParams{Limit: int64(limit), Offset: int64(offset)})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
total, err := q.CountLanguages(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
selfURL := buildURL("/languages", limit, offset, order, c.Query("expand"))
|
||||
res := hres.New(selfURL)
|
||||
res.AddCurie()
|
||||
res.AddLink("self", halgo.Link{Href: selfURL})
|
||||
if int64(offset+limit) < total {
|
||||
nextURL := buildURL("/languages", limit, offset+limit, order, c.Query("expand"))
|
||||
res.AddLink("next", halgo.Link{Href: nextURL})
|
||||
}
|
||||
if offset > 0 {
|
||||
prev := offset - limit
|
||||
if prev < 0 {
|
||||
prev = 0
|
||||
}
|
||||
prevURL := buildURL("/languages", limit, prev, order, c.Query("expand"))
|
||||
res.AddLink("prev", halgo.Link{Href: prevURL})
|
||||
}
|
||||
|
||||
var embedded []interface{}
|
||||
for _, l := range langs {
|
||||
itemURL := "/languages/" + strconv.FormatInt(l.LanguageID, 10)
|
||||
item := hres.New(itemURL)
|
||||
item.SetState(l)
|
||||
item.AddLink("self", halgo.Link{Href: itemURL})
|
||||
// Convenience link to films filtered by this language name
|
||||
name := strings.TrimSpace(fmt.Sprint(l.Name))
|
||||
if name != "" {
|
||||
qv := url.Values{"language.name": []string{name}}
|
||||
item.AddLink("sakila:films", halgo.Link{Href: "/films?" + qv.Encode()})
|
||||
}
|
||||
embedded = append(embedded, item)
|
||||
}
|
||||
res.Embed("languages", embedded)
|
||||
res.SetState(gin.H{"count": len(langs), "total": total, "limit": limit, "offset": offset})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
|
||||
// Get language
|
||||
r.GET("/languages/:id", func(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
q := sq.New(db)
|
||||
l, err := q.GetLanguage(c, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(404, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
self := "/languages/" + strconv.FormatInt(l.LanguageID, 10)
|
||||
res := hres.New(self)
|
||||
res.AddCurie()
|
||||
res.SetState(l)
|
||||
res.AddLink("self", halgo.Link{Href: self})
|
||||
name := strings.TrimSpace(fmt.Sprint(l.Name))
|
||||
if name != "" {
|
||||
qv := url.Values{"language.name": []string{name}}
|
||||
res.AddLink("sakila:films", halgo.Link{Href: "/films?" + qv.Encode()})
|
||||
}
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, res)
|
||||
})
|
||||
}
|
||||
22
internal/httpapi/root.go
Normal file
22
internal/httpapi/root.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
hres "form-builder-be-go/internal/hal"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/raff/halgo"
|
||||
)
|
||||
|
||||
func RegisterRoot(r *gin.Engine, db *sql.DB) {
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
root := hres.New("/")
|
||||
root.AddCurie()
|
||||
root.AddLink("sakila:actors", halgo.Link{Href: "/actors"})
|
||||
root.AddLink("sakila:films", halgo.Link{Href: "/films"})
|
||||
root.AddLink("sakila:languages", halgo.Link{Href: "/languages"})
|
||||
root.AddLink("sakila:categories", halgo.Link{Href: "/categories"})
|
||||
c.Header("Content-Type", "application/hal+json")
|
||||
c.JSON(200, root)
|
||||
})
|
||||
}
|
||||
53
internal/httpapi/util.go
Normal file
53
internal/httpapi/util.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseIntDefault(s string, def int) int {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func parseExpand(s string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return m
|
||||
}
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
m[p] = true
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func buildURL(base string, limit, offset int, order, expand string) string {
|
||||
v := url.Values{}
|
||||
if limit > 0 {
|
||||
v.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
if offset > 0 {
|
||||
v.Set("offset", strconv.Itoa(offset))
|
||||
}
|
||||
if strings.TrimSpace(order) != "" {
|
||||
v.Set("order", order)
|
||||
}
|
||||
if strings.TrimSpace(expand) != "" {
|
||||
v.Set("expand", expand)
|
||||
}
|
||||
qs := v.Encode()
|
||||
if qs == "" {
|
||||
return base
|
||||
}
|
||||
return base + "?" + qs
|
||||
}
|
||||
219
internal/sqlc/actor.sql.go
Normal file
219
internal/sqlc/actor.sql.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: actor.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const countActors = `-- name: CountActors :one
|
||||
SELECT COUNT(*)
|
||||
FROM actor
|
||||
`
|
||||
|
||||
func (q *Queries) CountActors(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countActors)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const deleteActor = `-- name: DeleteActor :execrows
|
||||
DELETE FROM actor WHERE actor_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteActor(ctx context.Context, actorID int64) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, deleteActor, actorID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const getActor = `-- name: GetActor :one
|
||||
SELECT actor_id, first_name, last_name, last_update
|
||||
FROM actor
|
||||
WHERE actor_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetActor(ctx context.Context, actorID int64) (Actor, error) {
|
||||
row := q.db.QueryRowContext(ctx, getActor, actorID)
|
||||
var i Actor
|
||||
err := row.Scan(
|
||||
&i.ActorID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.LastUpdate,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertActor = `-- name: InsertActor :exec
|
||||
INSERT INTO actor (actor_id, first_name, last_name, last_update)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`
|
||||
|
||||
type InsertActorParams struct {
|
||||
ActorID int64 `json:"actor_id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertActor(ctx context.Context, arg InsertActorParams) error {
|
||||
_, err := q.db.ExecContext(ctx, insertActor, arg.ActorID, arg.FirstName, arg.LastName)
|
||||
return err
|
||||
}
|
||||
|
||||
const listActors = `-- name: ListActors :many
|
||||
SELECT actor_id, first_name, last_name, last_update
|
||||
FROM actor
|
||||
ORDER BY actor_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListActorsParams struct {
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListActors(ctx context.Context, arg ListActorsParams) ([]Actor, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listActors, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Actor
|
||||
for rows.Next() {
|
||||
var i Actor
|
||||
if err := rows.Scan(
|
||||
&i.ActorID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listFilmsByActor = `-- name: ListFilmsByActor :many
|
||||
SELECT f.film_id,
|
||||
f.title,
|
||||
f.description,
|
||||
f.release_year,
|
||||
f.language_id,
|
||||
f.original_language_id,
|
||||
f.rental_duration,
|
||||
f.rental_rate,
|
||||
f.length,
|
||||
f.replacement_cost,
|
||||
f.rating,
|
||||
f.special_features,
|
||||
f.last_update
|
||||
FROM film f
|
||||
JOIN film_actor fa ON fa.film_id = f.film_id
|
||||
WHERE fa.actor_id = ?
|
||||
ORDER BY f.title ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFilmsByActor(ctx context.Context, actorID int64) ([]Film, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilmsByActor, actorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Film
|
||||
for rows.Next() {
|
||||
var i Film
|
||||
if err := rows.Scan(
|
||||
&i.FilmID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ReleaseYear,
|
||||
&i.LanguageID,
|
||||
&i.OriginalLanguageID,
|
||||
&i.RentalDuration,
|
||||
&i.RentalRate,
|
||||
&i.Length,
|
||||
&i.ReplacementCost,
|
||||
&i.Rating,
|
||||
&i.SpecialFeatures,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const nextActorID = `-- name: NextActorID :one
|
||||
SELECT COALESCE(MAX(actor_id), 0) + 1 FROM actor
|
||||
`
|
||||
|
||||
func (q *Queries) NextActorID(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, nextActorID)
|
||||
var column_1 int64
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const patchActor = `-- name: PatchActor :execrows
|
||||
UPDATE actor
|
||||
SET first_name = COALESCE(?1, first_name),
|
||||
last_name = COALESCE(?2, last_name)
|
||||
WHERE actor_id = ?3
|
||||
`
|
||||
|
||||
type PatchActorParams struct {
|
||||
FirstName sql.NullString `json:"first_name"`
|
||||
LastName sql.NullString `json:"last_name"`
|
||||
ActorID int64 `json:"actor_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) PatchActor(ctx context.Context, arg PatchActorParams) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, patchActor, arg.FirstName, arg.LastName, arg.ActorID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const updateActorPut = `-- name: UpdateActorPut :execrows
|
||||
UPDATE actor
|
||||
SET first_name = ?,
|
||||
last_name = ?
|
||||
WHERE actor_id = ?
|
||||
`
|
||||
|
||||
type UpdateActorPutParams struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
ActorID int64 `json:"actor_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateActorPut(ctx context.Context, arg UpdateActorPutParams) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, updateActorPut, arg.FirstName, arg.LastName, arg.ActorID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
139
internal/sqlc/categories.sql.go
Normal file
139
internal/sqlc/categories.sql.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: categories.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const countCategories = `-- name: CountCategories :one
|
||||
SELECT COUNT(*) FROM category
|
||||
`
|
||||
|
||||
func (q *Queries) CountCategories(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countCategories)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countFilmsByCategoryName = `-- name: CountFilmsByCategoryName :one
|
||||
SELECT COUNT(*)
|
||||
FROM film f
|
||||
JOIN film_category fc ON fc.film_id = f.film_id
|
||||
JOIN category c ON c.category_id = fc.category_id
|
||||
WHERE UPPER(c.name) = UPPER(?)
|
||||
`
|
||||
|
||||
func (q *Queries) CountFilmsByCategoryName(ctx context.Context, upper string) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countFilmsByCategoryName, upper)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getCategory = `-- name: GetCategory :one
|
||||
SELECT category_id, name, last_update
|
||||
FROM category
|
||||
WHERE category_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetCategory(ctx context.Context, categoryID int64) (Category, error) {
|
||||
row := q.db.QueryRowContext(ctx, getCategory, categoryID)
|
||||
var i Category
|
||||
err := row.Scan(&i.CategoryID, &i.Name, &i.LastUpdate)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listCategories = `-- name: ListCategories :many
|
||||
SELECT category_id, name, last_update
|
||||
FROM category
|
||||
ORDER BY category_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListCategoriesParams struct {
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListCategories(ctx context.Context, arg ListCategoriesParams) ([]Category, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listCategories, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Category
|
||||
for rows.Next() {
|
||||
var i Category
|
||||
if err := rows.Scan(&i.CategoryID, &i.Name, &i.LastUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listFilmsByCategoryName = `-- name: ListFilmsByCategoryName :many
|
||||
SELECT f.film_id, f.title, f.description, f.release_year, f.language_id,
|
||||
f.original_language_id, f.rental_duration, f.rental_rate, f.length,
|
||||
f.replacement_cost, f.rating, f.special_features, f.last_update
|
||||
FROM film f
|
||||
JOIN film_category fc ON fc.film_id = f.film_id
|
||||
JOIN category c ON c.category_id = fc.category_id
|
||||
WHERE UPPER(c.name) = UPPER(?)
|
||||
ORDER BY f.film_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListFilmsByCategoryNameParams struct {
|
||||
UPPER string `json:"UPPER"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListFilmsByCategoryName(ctx context.Context, arg ListFilmsByCategoryNameParams) ([]Film, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilmsByCategoryName, arg.UPPER, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Film
|
||||
for rows.Next() {
|
||||
var i Film
|
||||
if err := rows.Scan(
|
||||
&i.FilmID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ReleaseYear,
|
||||
&i.LanguageID,
|
||||
&i.OriginalLanguageID,
|
||||
&i.RentalDuration,
|
||||
&i.RentalRate,
|
||||
&i.Length,
|
||||
&i.ReplacementCost,
|
||||
&i.Rating,
|
||||
&i.SpecialFeatures,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
31
internal/sqlc/db.go
Normal file
31
internal/sqlc/db.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
158
internal/sqlc/film.sql.go
Normal file
158
internal/sqlc/film.sql.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: film.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const countFilms = `-- name: CountFilms :one
|
||||
SELECT COUNT(*)
|
||||
FROM film
|
||||
`
|
||||
|
||||
func (q *Queries) CountFilms(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countFilms)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getFilm = `-- name: GetFilm :one
|
||||
SELECT film_id,
|
||||
title,
|
||||
description,
|
||||
release_year,
|
||||
language_id,
|
||||
original_language_id,
|
||||
rental_duration,
|
||||
rental_rate,
|
||||
length,
|
||||
replacement_cost,
|
||||
rating,
|
||||
special_features,
|
||||
last_update
|
||||
FROM film
|
||||
WHERE film_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetFilm(ctx context.Context, filmID int64) (Film, error) {
|
||||
row := q.db.QueryRowContext(ctx, getFilm, filmID)
|
||||
var i Film
|
||||
err := row.Scan(
|
||||
&i.FilmID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ReleaseYear,
|
||||
&i.LanguageID,
|
||||
&i.OriginalLanguageID,
|
||||
&i.RentalDuration,
|
||||
&i.RentalRate,
|
||||
&i.Length,
|
||||
&i.ReplacementCost,
|
||||
&i.Rating,
|
||||
&i.SpecialFeatures,
|
||||
&i.LastUpdate,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listActorsByFilm = `-- name: ListActorsByFilm :many
|
||||
SELECT a.actor_id, a.first_name, a.last_name, a.last_update
|
||||
FROM actor a
|
||||
JOIN film_actor fa ON fa.actor_id = a.actor_id
|
||||
WHERE fa.film_id = ?
|
||||
ORDER BY a.last_name ASC, a.first_name ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListActorsByFilm(ctx context.Context, filmID int64) ([]Actor, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listActorsByFilm, filmID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Actor
|
||||
for rows.Next() {
|
||||
var i Actor
|
||||
if err := rows.Scan(
|
||||
&i.ActorID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listFilms = `-- name: ListFilms :many
|
||||
SELECT film_id,
|
||||
title,
|
||||
description,
|
||||
release_year,
|
||||
language_id,
|
||||
original_language_id,
|
||||
rental_duration,
|
||||
rental_rate,
|
||||
length,
|
||||
replacement_cost,
|
||||
rating,
|
||||
special_features,
|
||||
last_update
|
||||
FROM film
|
||||
ORDER BY film_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListFilmsParams struct {
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListFilms(ctx context.Context, arg ListFilmsParams) ([]Film, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilms, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Film
|
||||
for rows.Next() {
|
||||
var i Film
|
||||
if err := rows.Scan(
|
||||
&i.FilmID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ReleaseYear,
|
||||
&i.LanguageID,
|
||||
&i.OriginalLanguageID,
|
||||
&i.RentalDuration,
|
||||
&i.RentalRate,
|
||||
&i.Length,
|
||||
&i.ReplacementCost,
|
||||
&i.Rating,
|
||||
&i.SpecialFeatures,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
78
internal/sqlc/film_filter_language.sql.go
Normal file
78
internal/sqlc/film_filter_language.sql.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: film_filter_language.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const countFilmsByLanguageName = `-- name: CountFilmsByLanguageName :one
|
||||
SELECT COUNT(*)
|
||||
FROM film f
|
||||
JOIN language l ON l.language_id = f.language_id
|
||||
WHERE UPPER(l.name) = UPPER(?)
|
||||
`
|
||||
|
||||
func (q *Queries) CountFilmsByLanguageName(ctx context.Context, upper string) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countFilmsByLanguageName, upper)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const listFilmsByLanguageName = `-- name: ListFilmsByLanguageName :many
|
||||
SELECT f.film_id, f.title, f.description, f.release_year, f.language_id,
|
||||
f.original_language_id, f.rental_duration, f.rental_rate, f.length,
|
||||
f.replacement_cost, f.rating, f.special_features, f.last_update
|
||||
FROM film f
|
||||
JOIN language l ON l.language_id = f.language_id
|
||||
WHERE UPPER(l.name) = UPPER(?)
|
||||
ORDER BY f.film_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListFilmsByLanguageNameParams struct {
|
||||
UPPER string `json:"UPPER"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListFilmsByLanguageName(ctx context.Context, arg ListFilmsByLanguageNameParams) ([]Film, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilmsByLanguageName, arg.UPPER, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Film
|
||||
for rows.Next() {
|
||||
var i Film
|
||||
if err := rows.Scan(
|
||||
&i.FilmID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ReleaseYear,
|
||||
&i.LanguageID,
|
||||
&i.OriginalLanguageID,
|
||||
&i.RentalDuration,
|
||||
&i.RentalRate,
|
||||
&i.Length,
|
||||
&i.ReplacementCost,
|
||||
&i.Rating,
|
||||
&i.SpecialFeatures,
|
||||
&i.LastUpdate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
70
internal/sqlc/language.sql.go
Normal file
70
internal/sqlc/language.sql.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: language.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const countLanguages = `-- name: CountLanguages :one
|
||||
SELECT COUNT(*)
|
||||
FROM language
|
||||
`
|
||||
|
||||
func (q *Queries) CountLanguages(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countLanguages)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getLanguage = `-- name: GetLanguage :one
|
||||
SELECT language_id, name, last_update
|
||||
FROM language
|
||||
WHERE language_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetLanguage(ctx context.Context, languageID int64) (Language, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLanguage, languageID)
|
||||
var i Language
|
||||
err := row.Scan(&i.LanguageID, &i.Name, &i.LastUpdate)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listLanguages = `-- name: ListLanguages :many
|
||||
SELECT language_id, name, last_update
|
||||
FROM language
|
||||
ORDER BY language_id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListLanguagesParams struct {
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLanguages(ctx context.Context, arg ListLanguagesParams) ([]Language, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listLanguages, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Language
|
||||
for rows.Next() {
|
||||
var i Language
|
||||
if err := rows.Scan(&i.LanguageID, &i.Name, &i.LastUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
193
internal/sqlc/models.go
Normal file
193
internal/sqlc/models.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Actor struct {
|
||||
ActorID int64 `json:"actor_id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
AddressID int64 `json:"address_id"`
|
||||
Address string `json:"address"`
|
||||
Address2 sql.NullString `json:"address2"`
|
||||
District string `json:"district"`
|
||||
CityID int64 `json:"city_id"`
|
||||
PostalCode sql.NullString `json:"postal_code"`
|
||||
Phone string `json:"phone"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type City struct {
|
||||
CityID int64 `json:"city_id"`
|
||||
City string `json:"city"`
|
||||
CountryID int64 `json:"country_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
CountryID int64 `json:"country_id"`
|
||||
Country string `json:"country"`
|
||||
LastUpdate sql.NullTime `json:"last_update"`
|
||||
}
|
||||
|
||||
type Customer struct {
|
||||
CustomerID int64 `json:"customer_id"`
|
||||
StoreID int64 `json:"store_id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email sql.NullString `json:"email"`
|
||||
AddressID int64 `json:"address_id"`
|
||||
Active interface{} `json:"active"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type CustomerList struct {
|
||||
ID int64 `json:"id"`
|
||||
Name interface{} `json:"name"`
|
||||
Address string `json:"address"`
|
||||
ZipCode sql.NullString `json:"zip_code"`
|
||||
Phone string `json:"phone"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Notes string `json:"notes"`
|
||||
Sid int64 `json:"sid"`
|
||||
}
|
||||
|
||||
type Film struct {
|
||||
FilmID int64 `json:"film_id"`
|
||||
Title string `json:"title"`
|
||||
Description interface{} `json:"description"`
|
||||
ReleaseYear sql.NullString `json:"release_year"`
|
||||
LanguageID int64 `json:"language_id"`
|
||||
OriginalLanguageID sql.NullInt64 `json:"original_language_id"`
|
||||
RentalDuration int64 `json:"rental_duration"`
|
||||
RentalRate float64 `json:"rental_rate"`
|
||||
Length sql.NullInt64 `json:"length"`
|
||||
ReplacementCost float64 `json:"replacement_cost"`
|
||||
Rating sql.NullString `json:"rating"`
|
||||
SpecialFeatures sql.NullString `json:"special_features"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type FilmActor struct {
|
||||
ActorID int64 `json:"actor_id"`
|
||||
FilmID int64 `json:"film_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type FilmCategory struct {
|
||||
FilmID int64 `json:"film_id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type FilmList struct {
|
||||
Fid int64 `json:"fid"`
|
||||
Title string `json:"title"`
|
||||
Description interface{} `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Price float64 `json:"price"`
|
||||
Length sql.NullInt64 `json:"length"`
|
||||
Rating sql.NullString `json:"rating"`
|
||||
Actors interface{} `json:"actors"`
|
||||
}
|
||||
|
||||
type FilmText struct {
|
||||
FilmID int64 `json:"film_id"`
|
||||
Title string `json:"title"`
|
||||
Description interface{} `json:"description"`
|
||||
}
|
||||
|
||||
type Inventory struct {
|
||||
InventoryID int64 `json:"inventory_id"`
|
||||
FilmID int64 `json:"film_id"`
|
||||
StoreID int64 `json:"store_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
LanguageID int64 `json:"language_id"`
|
||||
Name interface{} `json:"name"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
PaymentID int64 `json:"payment_id"`
|
||||
CustomerID int64 `json:"customer_id"`
|
||||
StaffID int64 `json:"staff_id"`
|
||||
RentalID sql.NullInt64 `json:"rental_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
PaymentDate time.Time `json:"payment_date"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type Rental struct {
|
||||
RentalID int64 `json:"rental_id"`
|
||||
RentalDate time.Time `json:"rental_date"`
|
||||
InventoryID int64 `json:"inventory_id"`
|
||||
CustomerID int64 `json:"customer_id"`
|
||||
ReturnDate sql.NullTime `json:"return_date"`
|
||||
StaffID int64 `json:"staff_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type SalesByFilmCategory struct {
|
||||
Category string `json:"category"`
|
||||
TotalSales sql.NullFloat64 `json:"total_sales"`
|
||||
}
|
||||
|
||||
type SalesByStore struct {
|
||||
StoreID int64 `json:"store_id"`
|
||||
Store interface{} `json:"store"`
|
||||
Manager interface{} `json:"manager"`
|
||||
TotalSales sql.NullFloat64 `json:"total_sales"`
|
||||
}
|
||||
|
||||
type Staff struct {
|
||||
StaffID int64 `json:"staff_id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
AddressID int64 `json:"address_id"`
|
||||
Picture []byte `json:"picture"`
|
||||
Email sql.NullString `json:"email"`
|
||||
StoreID int64 `json:"store_id"`
|
||||
Active int64 `json:"active"`
|
||||
Username string `json:"username"`
|
||||
Password sql.NullString `json:"password"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
type StaffList struct {
|
||||
ID int64 `json:"id"`
|
||||
Name interface{} `json:"name"`
|
||||
Address string `json:"address"`
|
||||
ZipCode sql.NullString `json:"zip_code"`
|
||||
Phone string `json:"phone"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Sid int64 `json:"sid"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
StoreID int64 `json:"store_id"`
|
||||
ManagerStaffID int64 `json:"manager_staff_id"`
|
||||
AddressID int64 `json:"address_id"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
66
main.go
Normal file
66
main.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"form-builder-be-go/internal/httpapi"
|
||||
)
|
||||
|
||||
func getEnv(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func CorsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // Or specify specific origins
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
// Handle OPTIONS preflight request
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// DB path can be overridden via env
|
||||
dbPath := getEnv("SAKILA_DB", "sakila-sqlite3/sakila_master.db")
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// Add the CORS middleware
|
||||
r.Use(CorsMiddleware())
|
||||
|
||||
httpapi.RegisterRoot(r, db)
|
||||
httpapi.RegisterActorRoutes(r, db)
|
||||
httpapi.RegisterFilmRoutes(r, db)
|
||||
httpapi.RegisterLanguageRoutes(r, db)
|
||||
httpapi.RegisterCategoryRoutes(r, db)
|
||||
|
||||
// Start server
|
||||
addr := getEnv("ADDR", ":8080")
|
||||
log.Printf("Listening on %s (DB=%s)", addr, dbPath)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
55
queries/actor.sql
Normal file
55
queries/actor.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- name: ListActors :many
|
||||
SELECT actor_id, first_name, last_name, last_update
|
||||
FROM actor
|
||||
ORDER BY actor_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountActors :one
|
||||
SELECT COUNT(*)
|
||||
FROM actor;
|
||||
|
||||
-- name: GetActor :one
|
||||
SELECT actor_id, first_name, last_name, last_update
|
||||
FROM actor
|
||||
WHERE actor_id = ?;
|
||||
|
||||
-- name: ListFilmsByActor :many
|
||||
SELECT f.film_id,
|
||||
f.title,
|
||||
f.description,
|
||||
f.release_year,
|
||||
f.language_id,
|
||||
f.original_language_id,
|
||||
f.rental_duration,
|
||||
f.rental_rate,
|
||||
f.length,
|
||||
f.replacement_cost,
|
||||
f.rating,
|
||||
f.special_features,
|
||||
f.last_update
|
||||
FROM film f
|
||||
JOIN film_actor fa ON fa.film_id = f.film_id
|
||||
WHERE fa.actor_id = ?
|
||||
ORDER BY f.title ASC;
|
||||
|
||||
-- name: NextActorID :one
|
||||
SELECT COALESCE(MAX(actor_id), 0) + 1 FROM actor;
|
||||
|
||||
-- name: InsertActor :exec
|
||||
INSERT INTO actor (actor_id, first_name, last_name, last_update)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP);
|
||||
|
||||
-- name: UpdateActorPut :execrows
|
||||
UPDATE actor
|
||||
SET first_name = ?,
|
||||
last_name = ?
|
||||
WHERE actor_id = ?;
|
||||
|
||||
-- name: PatchActor :execrows
|
||||
UPDATE actor
|
||||
SET first_name = COALESCE(sqlc.narg(first_name), first_name),
|
||||
last_name = COALESCE(sqlc.narg(last_name), last_name)
|
||||
WHERE actor_id = sqlc.arg(actor_id);
|
||||
|
||||
-- name: DeleteActor :execrows
|
||||
DELETE FROM actor WHERE actor_id = ?;
|
||||
31
queries/categories.sql
Normal file
31
queries/categories.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- name: ListCategories :many
|
||||
SELECT category_id, name, last_update
|
||||
FROM category
|
||||
ORDER BY category_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountCategories :one
|
||||
SELECT COUNT(*) FROM category;
|
||||
|
||||
-- name: GetCategory :one
|
||||
SELECT category_id, name, last_update
|
||||
FROM category
|
||||
WHERE category_id = ?;
|
||||
|
||||
-- name: ListFilmsByCategoryName :many
|
||||
SELECT f.film_id, f.title, f.description, f.release_year, f.language_id,
|
||||
f.original_language_id, f.rental_duration, f.rental_rate, f.length,
|
||||
f.replacement_cost, f.rating, f.special_features, f.last_update
|
||||
FROM film f
|
||||
JOIN film_category fc ON fc.film_id = f.film_id
|
||||
JOIN category c ON c.category_id = fc.category_id
|
||||
WHERE UPPER(c.name) = UPPER(?)
|
||||
ORDER BY f.film_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountFilmsByCategoryName :one
|
||||
SELECT COUNT(*)
|
||||
FROM film f
|
||||
JOIN film_category fc ON fc.film_id = f.film_id
|
||||
JOIN category c ON c.category_id = fc.category_id
|
||||
WHERE UPPER(c.name) = UPPER(?);
|
||||
45
queries/film.sql
Normal file
45
queries/film.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
-- name: ListFilms :many
|
||||
SELECT film_id,
|
||||
title,
|
||||
description,
|
||||
release_year,
|
||||
language_id,
|
||||
original_language_id,
|
||||
rental_duration,
|
||||
rental_rate,
|
||||
length,
|
||||
replacement_cost,
|
||||
rating,
|
||||
special_features,
|
||||
last_update
|
||||
FROM film
|
||||
ORDER BY film_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountFilms :one
|
||||
SELECT COUNT(*)
|
||||
FROM film;
|
||||
|
||||
-- name: GetFilm :one
|
||||
SELECT film_id,
|
||||
title,
|
||||
description,
|
||||
release_year,
|
||||
language_id,
|
||||
original_language_id,
|
||||
rental_duration,
|
||||
rental_rate,
|
||||
length,
|
||||
replacement_cost,
|
||||
rating,
|
||||
special_features,
|
||||
last_update
|
||||
FROM film
|
||||
WHERE film_id = ?;
|
||||
|
||||
-- name: ListActorsByFilm :many
|
||||
SELECT a.actor_id, a.first_name, a.last_name, a.last_update
|
||||
FROM actor a
|
||||
JOIN film_actor fa ON fa.actor_id = a.actor_id
|
||||
WHERE fa.film_id = ?
|
||||
ORDER BY a.last_name ASC, a.first_name ASC;
|
||||
15
queries/film_filter_language.sql
Normal file
15
queries/film_filter_language.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- name: ListFilmsByLanguageName :many
|
||||
SELECT f.film_id, f.title, f.description, f.release_year, f.language_id,
|
||||
f.original_language_id, f.rental_duration, f.rental_rate, f.length,
|
||||
f.replacement_cost, f.rating, f.special_features, f.last_update
|
||||
FROM film f
|
||||
JOIN language l ON l.language_id = f.language_id
|
||||
WHERE UPPER(l.name) = UPPER(?)
|
||||
ORDER BY f.film_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountFilmsByLanguageName :one
|
||||
SELECT COUNT(*)
|
||||
FROM film f
|
||||
JOIN language l ON l.language_id = f.language_id
|
||||
WHERE UPPER(l.name) = UPPER(?);
|
||||
14
queries/language.sql
Normal file
14
queries/language.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- name: ListLanguages :many
|
||||
SELECT language_id, name, last_update
|
||||
FROM language
|
||||
ORDER BY language_id ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CountLanguages :one
|
||||
SELECT COUNT(*)
|
||||
FROM language;
|
||||
|
||||
-- name: GetLanguage :one
|
||||
SELECT language_id, name, last_update
|
||||
FROM language
|
||||
WHERE language_id = ?;
|
||||
1
sakila-sqlite3
Submodule
1
sakila-sqlite3
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 9394b42d13888c3d3d3d56cd7e9c84fadafb71c7
|
||||
15
sqlc.yaml
Normal file
15
sqlc.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
version: "2"
|
||||
sql:
|
||||
- engine: "sqlite"
|
||||
schema:
|
||||
- "sakila-sqlite3-main/source/sqlite-sakila-schema.sql"
|
||||
queries:
|
||||
- "queries/*.sql"
|
||||
gen:
|
||||
go:
|
||||
package: "sqlc"
|
||||
out: "internal/sqlc"
|
||||
emit_json_tags: true
|
||||
emit_prepared_queries: false
|
||||
emit_interface: false
|
||||
emit_exact_table_names: true
|
||||
Loading…
Reference in a new issue