(Feat): Initial Commit
This commit is contained in:
42
scripts/README.md
Normal file
42
scripts/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# `/scripts`
|
||||
|
||||
Scripts to perform various build, install, analysis, etc operations.
|
||||
|
||||
These scripts keep the root level Makefile small and simple.
|
||||
|
||||
https://github.com/golang-standards/project-layout/tree/master/scripts
|
||||
|
||||
Examples:
|
||||
|
||||
* https://github.com/kubernetes/helm/tree/master/scripts
|
||||
* https://github.com/cockroachdb/cockroach/tree/master/scripts
|
||||
* https://github.com/hashicorp/terraform/tree/master/scripts
|
||||
|
||||
Please note that this scripts are not available in a final product. Head to `../cmd` if you need to execute your script in live environments.
|
||||
|
||||
The `gsdev` cli util executes this `scripts/main.go` file here and also describes all available commands available while developing a project locally. `gsdev` is made available during the `Dockerfile`'s development stage.
|
||||
|
||||
### `/scripts/cmd/*.go`
|
||||
|
||||
`func`s may define shared logic used in `/scripts/internal/**/*.go`, the actual usable commands are defined within `/scripts/internal`.
|
||||
|
||||
### `//go:build scripts`
|
||||
|
||||
Any `*.go` file in all subdirectories of `/scripts/**` should specify `//go:build scripts` to signal that those files are not part of of our final product. To execute any script that has this build tag, you need to specify `-tags scripts`, otherwise you will run into an error like the following (also see our `Makefile` for a reference):
|
||||
|
||||
```bash
|
||||
# Works
|
||||
go run -tags scripts scripts/main.go
|
||||
# go-starter development scripts
|
||||
# Utility commands while developing go-starter based projects.
|
||||
|
||||
# Works (same as above)
|
||||
gsdev
|
||||
# go-starter development scripts
|
||||
# Utility commands while developing go-starter based projects.
|
||||
|
||||
# Misses build tag "scripts"
|
||||
go run scripts/main.go
|
||||
# package command-line-arguments
|
||||
# imports allaboutapps.dev/aw/go-starter/scripts/cmd: build constraints exclude all Go files in /app/scripts/cmd
|
||||
```
|
||||
29
scripts/cmd/handlers.go
Normal file
29
scripts/cmd/handlers.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// handlersCmd represents the handlers command
|
||||
// see handlers_*.go for sub_commands
|
||||
var handlersCmd = &cobra.Command{
|
||||
Use: "handlers <subcommand>",
|
||||
Short: "Handlers related subcommands",
|
||||
Run: func(cmd *cobra.Command, _ []string /* args */) {
|
||||
if err := cmd.Help(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to print help")
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(handlersCmd)
|
||||
}
|
||||
37
scripts/cmd/handlers_check.go
Normal file
37
scripts/cmd/handlers_check.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/handlers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
printAllFlag = "print-all"
|
||||
)
|
||||
|
||||
var handlersCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Checks handlers vs. swagger spec.",
|
||||
Long: `Checks currently implemented handlers against swagger spec.`,
|
||||
Run: func(cmd *cobra.Command, _ []string /* args */) {
|
||||
|
||||
printAll, err := cmd.Flags().GetBool(printAllFlag)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = handlers.CheckHandlers(printAll)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
handlersCmd.AddCommand(handlersCheckCmd)
|
||||
handlersCheckCmd.Flags().Bool(printAllFlag, false, "Print only print the current implemented handlers, do not generate the file.")
|
||||
}
|
||||
38
scripts/cmd/handlers_gen.go
Normal file
38
scripts/cmd/handlers_gen.go
Normal file
@@ -0,0 +1,38 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/handlers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
printOnlyFlag = "print-only"
|
||||
)
|
||||
|
||||
var handlersGenCmd = &cobra.Command{
|
||||
Use: "gen",
|
||||
Short: "Generate internal/api/handlers/handlers.go.",
|
||||
Long: `Generates internal/api/handlers/handlers.go file based on the current implemented handlers.`,
|
||||
Run: func(cmd *cobra.Command, _ []string /* args */) {
|
||||
|
||||
printOnly, err := cmd.Flags().GetBool(printOnlyFlag)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = handlers.GenHandlers(printOnly)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
handlersCmd.AddCommand(handlersGenCmd)
|
||||
handlersGenCmd.Flags().Bool(printOnlyFlag, false, "Print all checked handlers regardless of errors.")
|
||||
}
|
||||
34
scripts/cmd/modulename.go
Normal file
34
scripts/cmd/modulename.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// moduleCmd represents the server command
|
||||
var moduleCmd = &cobra.Command{
|
||||
Use: "modulename",
|
||||
Short: "Prints the modulename",
|
||||
Long: `Prints the currently applied go modulename of this project.`,
|
||||
Run: func(_ *cobra.Command /* cmd */, _ []string /* args */) {
|
||||
runModulename()
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(moduleCmd)
|
||||
}
|
||||
|
||||
func runModulename() {
|
||||
baseModuleName, err := util.GetModuleName(modulePath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to get module name")
|
||||
}
|
||||
|
||||
log.Info().Msg(baseModuleName)
|
||||
}
|
||||
34
scripts/cmd/root.go
Normal file
34
scripts/cmd/root.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
projectRoot = util.GetProjectRootDir()
|
||||
modulePath = filepath.Join(util.GetProjectRootDir(), "go.mod")
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "gsdev",
|
||||
Short: "gsdev",
|
||||
Long: `go-starter development scripts
|
||||
Utility commands while developing go-starter based projects.`,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to execute root command")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
86
scripts/cmd/scaffold.go
Normal file
86
scripts/cmd/scaffold.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//go:build scripts
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/scaffold"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
methodsFlag = "methods"
|
||||
forceFlag = "force"
|
||||
)
|
||||
|
||||
var (
|
||||
modelPath = filepath.Join(projectRoot, "internal/models")
|
||||
swaggerPath = filepath.Join(projectRoot, "api")
|
||||
handlerPath = filepath.Join(projectRoot, "internal/api/handlers")
|
||||
|
||||
makeSwaggerCmd = exec.Command("make", "swagger")
|
||||
makeGoGenerateCmd = exec.Command("make", "go-generate")
|
||||
|
||||
defaultMethods = []string{"get-all", "get", "post", "put", "delete"}
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var scaffoldCmd = &cobra.Command{
|
||||
Use: "scaffold [resource name]",
|
||||
Short: "Scaffolding tool for CRUD endpoints.",
|
||||
Long: "Scaffolding tool to generate CRUD endpoint stubs from sqlboiler model definitions.",
|
||||
Run: generate,
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(scaffoldCmd)
|
||||
scaffoldCmd.Flags().StringSliceP(methodsFlag, "m", defaultMethods, "Specify HTTP methods to generate handlers for. Example: scaffold --methods get-all,get,delete")
|
||||
scaffoldCmd.Flags().BoolP(forceFlag, "f", false, "Forces the tool to overwrite existing files.")
|
||||
}
|
||||
|
||||
func generate(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("Please provide a valid resource name")
|
||||
}
|
||||
|
||||
resourceName := args[0]
|
||||
if resourceName == "" {
|
||||
log.Fatal("Please provide a valid resource name")
|
||||
}
|
||||
|
||||
methods, err := cmd.Flags().GetStringSlice(methodsFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read %s: %v", methodsFlag, err)
|
||||
}
|
||||
|
||||
force, err := cmd.Flags().GetBool(forceFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read %s: %v", forceFlag, err)
|
||||
}
|
||||
|
||||
resourcePath := filepath.Join(modelPath, resourceName+".go")
|
||||
resource, err := scaffold.ParseModel(resourcePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse resource from model file: %v", err)
|
||||
}
|
||||
|
||||
if err = scaffold.GenerateSwagger(resource, swaggerPath, force); err != nil {
|
||||
log.Fatalf("Failed to generate Swagger spec: %v", err)
|
||||
}
|
||||
|
||||
if err = scaffold.GenerateHandlers(resource, handlerPath, modulePath, methods, force); err != nil {
|
||||
log.Fatalf("Failed to generate handlers: %v", err)
|
||||
}
|
||||
|
||||
if err = makeSwaggerCmd.Run(); err != nil {
|
||||
log.Fatalf("Failed to run 'make swagger': %v", err)
|
||||
}
|
||||
|
||||
if err = makeGoGenerateCmd.Run(); err != nil {
|
||||
log.Fatalf("Failed to run 'make go-generate': %v", err)
|
||||
}
|
||||
}
|
||||
120
scripts/internal/handlers/check.go
Normal file
120
scripts/internal/handlers/check.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//go:build scripts
|
||||
|
||||
// This program checks /internal/api/handlers.go and /internal/types/spec_handlers.go
|
||||
// It can be invoked by running go run -tags scripts scripts/handlers/check_handlers.go
|
||||
|
||||
// Supported args:
|
||||
// --print-all
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/router"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func CheckHandlers(printAll bool) error {
|
||||
// we initialize a minimal echo server without any other deps
|
||||
// so we can attach the current defined routes and read them
|
||||
log.Logger = log.Output(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.TimeFormat = "15:04:05"
|
||||
}))
|
||||
|
||||
defaultConfig := config.DefaultServiceConfigFromEnv()
|
||||
defaultConfig.Echo.ListenAddress = ":0"
|
||||
|
||||
s := api.NewServer(defaultConfig)
|
||||
err := router.Init(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize router: %w", err)
|
||||
}
|
||||
|
||||
// swaggerspec vs routes
|
||||
routes := s.Router.Routes
|
||||
swaggerSpec := types.NewSwaggerSpec()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, route := range routes {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// replace named echo ":param" to swagger path params "{param}" (curly braces) to properly match paths
|
||||
fragments := strings.Split(route.Path, "/")
|
||||
|
||||
for i, fragment := range fragments {
|
||||
if strings.HasPrefix(fragment, ":") {
|
||||
fragments[i] = "{" + strings.TrimLeft(fragment, ":") + "}"
|
||||
}
|
||||
}
|
||||
|
||||
swaggerPath := strings.Join(fragments, "/")
|
||||
|
||||
ok := swaggerSpec.Handlers[route.Method][swaggerPath]
|
||||
|
||||
if !ok {
|
||||
log.Warn().Msgf("%s %s\n WARNING: Missing swagger spec in api/swagger.yml!", route.Method, route.Path)
|
||||
} else if printAll {
|
||||
log.Info().Msgf("%s %s", route.Method, route.Path)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for method, v := range swaggerSpec.Handlers {
|
||||
for path := range v {
|
||||
// NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
|
||||
ttMethod := method
|
||||
ttPath := path
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// replace named swagger path params "{param}" to echo ":param" (dotted) to properly match paths
|
||||
fragments := strings.Split(ttPath, "/")
|
||||
|
||||
for i, fragment := range fragments {
|
||||
if strings.HasPrefix(fragment, "{") && strings.HasSuffix(fragment, "}") {
|
||||
fragments[i] = ":" + strings.TrimLeft(strings.TrimRight(fragment, "}"), "{")
|
||||
}
|
||||
}
|
||||
|
||||
echoPath := strings.Join(fragments, "/")
|
||||
|
||||
hasMatch := false
|
||||
|
||||
for _, route := range routes {
|
||||
if route.Method == ttMethod && route.Path == echoPath {
|
||||
hasMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasMatch {
|
||||
log.Warn().Msgf("%s %s\n WARNING: Missing route implementation in internal/api/handlers/*!", ttMethod, echoPath)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// we have our routes, the server is no longer needed.
|
||||
if errs := s.Shutdown(context.Background()); len(errs) > 0 {
|
||||
log.Error().Errs("shutdownErrors", errs).Msg("Failed to stop introspection server")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
183
scripts/internal/handlers/gen.go
Normal file
183
scripts/internal/handlers/gen.go
Normal file
@@ -0,0 +1,183 @@
|
||||
//go:build scripts
|
||||
|
||||
// This program generates /internal/api/handlers.go.
|
||||
// It can be invoked by running go run -tags scripts scripts/handlers/gen_handlers.go
|
||||
|
||||
// Supported args:
|
||||
// -print-only
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/util"
|
||||
)
|
||||
|
||||
// https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/
|
||||
|
||||
var (
|
||||
handlersPackage = "/internal/api/handlers"
|
||||
|
||||
pathProjectRoot = util.GetProjectRootDir()
|
||||
pathHandlersRoot = pathProjectRoot + handlersPackage
|
||||
pathModFile = pathProjectRoot + "/go.mod"
|
||||
pathHandlersFile = pathHandlersRoot + "/handlers.go"
|
||||
|
||||
// methodPrefixes defines all keywords that we search for in the handlers sub packages
|
||||
// <methodPrefixes>*<methodSuffix> must be the naming of the route (Capitalized)
|
||||
methodPrefixes = []string{
|
||||
// https://swagger.io/specification/v2/ fixed fields: GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH
|
||||
"Get", "Put", "Post", "Delete", "Options", "Head", "Patch",
|
||||
}
|
||||
// Match suffixes like 'Route', 'RouteV1', 'RouteV123'
|
||||
methodSuffixPattern = regexp.MustCompile(`Route(V[0-9]*)?$`)
|
||||
|
||||
packageTemplate = template.Must(template.New("").Parse(`// Code generated by go run -tags scripts scripts/handlers/gen_handlers.go; DO NOT EDIT.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
{{- range .SubPkgs }}
|
||||
"{{ . }}"
|
||||
{{- end }}
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func AttachAllRoutes(s *api.Server) {
|
||||
// attach our routes
|
||||
s.Router.Routes = []*echo.Route{
|
||||
{{- range .Funcs }}
|
||||
{{ .PackageName }}.{{ .FunctionName }}(s),
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
`))
|
||||
)
|
||||
|
||||
type ResolvedFunction struct {
|
||||
FunctionName string
|
||||
PackageName string
|
||||
PackageNameFQDN string
|
||||
}
|
||||
|
||||
type TempateData struct {
|
||||
SubPkgs []string
|
||||
Funcs []ResolvedFunction
|
||||
}
|
||||
|
||||
// get all functions in above handler packages
|
||||
// that match <methodPrefixes>*<methodSuffix>
|
||||
func GenHandlers(printOnly bool) error {
|
||||
funcs := []ResolvedFunction{}
|
||||
|
||||
baseModuleName, err := util.GetModuleName(pathModFile)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to get module name")
|
||||
}
|
||||
|
||||
set := token.NewFileSet()
|
||||
|
||||
err = filepath.Walk(pathHandlersRoot, func(path string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to access path %q: %w", path, err)
|
||||
}
|
||||
|
||||
// ignore handler file to be generated
|
||||
// ignore directories
|
||||
// ignore non go files
|
||||
// ignore test go files
|
||||
if path == pathHandlersFile || fileInfo.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "test.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
gofile, err := parser.ParseFile(set, path, nil, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse package: %q: %w", path, err)
|
||||
}
|
||||
|
||||
fileDir := filepath.Dir(path)
|
||||
packageNameFQDN := strings.Replace(fileDir, pathProjectRoot, baseModuleName, 1)
|
||||
|
||||
for _, d := range gofile.Decls {
|
||||
if fn, isFn := d.(*ast.FuncDecl); isFn {
|
||||
fnName := fn.Name.String()
|
||||
|
||||
for _, prefix := range methodPrefixes {
|
||||
if strings.HasPrefix(fnName, prefix) && methodSuffixPattern.MatchString(fnName) {
|
||||
funcs = append(funcs, ResolvedFunction{
|
||||
FunctionName: fnName,
|
||||
PackageName: gofile.Name.Name,
|
||||
PackageNameFQDN: packageNameFQDN,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fmt.Println(packageNameFQDN, path)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to walk handlers root: %w", err)
|
||||
}
|
||||
|
||||
// stable sort the functions
|
||||
// first PackageName then FunctionName
|
||||
sort.Slice(funcs, func(i, j int) bool {
|
||||
return funcs[i].PackageNameFQDN+funcs[i].FunctionName < funcs[j].PackageNameFQDN+funcs[j].FunctionName
|
||||
})
|
||||
|
||||
// only add subPkg if there was actually a route within it found
|
||||
subPkgs := []string{}
|
||||
for _, fun := range funcs {
|
||||
mustAppend := true
|
||||
|
||||
for _, a := range subPkgs {
|
||||
if a == fun.PackageNameFQDN {
|
||||
mustAppend = false
|
||||
}
|
||||
}
|
||||
|
||||
if mustAppend {
|
||||
subPkgs = append(subPkgs, fun.PackageNameFQDN)
|
||||
}
|
||||
}
|
||||
|
||||
if printOnly {
|
||||
for _, function := range funcs {
|
||||
log.Info().Msgf("%s %s", function.PackageNameFQDN, function.FunctionName)
|
||||
}
|
||||
|
||||
// bailout
|
||||
return nil
|
||||
}
|
||||
|
||||
handlersFile, err := os.Create(pathHandlersFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create handlers file: %w", err)
|
||||
}
|
||||
|
||||
defer handlersFile.Close()
|
||||
|
||||
if err = packageTemplate.Execute(handlersFile, TempateData{
|
||||
SubPkgs: subPkgs,
|
||||
Funcs: funcs,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to execute package template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
367
scripts/internal/scaffold/scaffold.go
Normal file
367
scripts/internal/scaffold/scaffold.go
Normal file
@@ -0,0 +1,367 @@
|
||||
//go:build scripts
|
||||
|
||||
package scaffold
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
// Scaffolding tool to auto-generate basic CRUD handlers for a given database model.
|
||||
|
||||
type FieldType struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Name string
|
||||
Type FieldType
|
||||
}
|
||||
|
||||
type StorageResource struct {
|
||||
Name string
|
||||
Fields []Field
|
||||
}
|
||||
|
||||
func ParseModel(path string) (*StorageResource, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read model file: %w", err)
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(`type ([^\s]+) (struct {[^}]+})`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile model expression regex: %w", err)
|
||||
}
|
||||
|
||||
matches := regex.FindStringSubmatch(string(content))
|
||||
resourceName := matches[1]
|
||||
expression := matches[2]
|
||||
|
||||
node, err := parser.ParseExpr(expression)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse model expression: %w", err)
|
||||
}
|
||||
|
||||
structType, ok := node.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not a struct definition", expression)
|
||||
}
|
||||
|
||||
fields := []Field{}
|
||||
for _, field := range structType.Fields.List {
|
||||
name := field.Names[0].Name
|
||||
fieldType := expression[field.Type.Pos()-1 : field.Type.End()-1]
|
||||
field := Field{
|
||||
Name: name,
|
||||
Type: FieldType{
|
||||
Name: fieldType,
|
||||
},
|
||||
}
|
||||
|
||||
if field.Name == "R" || field.Name == "L" {
|
||||
break
|
||||
}
|
||||
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
resource := StorageResource{
|
||||
Name: resourceName,
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Name string
|
||||
Type string
|
||||
Required bool
|
||||
Format *string
|
||||
}
|
||||
|
||||
type SwaggerResource struct {
|
||||
Name string
|
||||
URLName string
|
||||
Properties []Property
|
||||
PayloadProperties []Property
|
||||
}
|
||||
|
||||
func GenerateSwagger(resource *StorageResource, outputPath string, force bool) error {
|
||||
definitionsPath := filepath.Join(outputPath, "definitions")
|
||||
pathsPath := filepath.Join(outputPath, "paths")
|
||||
if err := createDirIfAbsent(definitionsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createDirIfAbsent(pathsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swaggerResource := toSwaggerResource(resource)
|
||||
definitionsSpecPath := filepath.Join(definitionsPath, swaggerResource.URLName+".yml")
|
||||
pathsSpecPath := filepath.Join(pathsPath, swaggerResource.URLName+".yml")
|
||||
|
||||
if err := executeTemplate(swaggerDefinitionsTemplate, definitionsSpecPath, swaggerResource, force); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return executeTemplate(swaggerPathsTemplate, pathsSpecPath, swaggerResource, force)
|
||||
}
|
||||
|
||||
var payloadExcluded = []string{"ID", "CreatedAt", "UpdatedAt"}
|
||||
|
||||
func toSwaggerResource(resource *StorageResource) *SwaggerResource {
|
||||
properties := make([]Property, 0, len(resource.Fields))
|
||||
payloadProperties := make([]Property, 0, len(resource.Fields))
|
||||
for _, field := range resource.Fields {
|
||||
property := fieldToProperty(field)
|
||||
properties = append(properties, property)
|
||||
|
||||
if !slices.Contains(payloadExcluded, field.Name) {
|
||||
payloadProperties = append(payloadProperties, property)
|
||||
}
|
||||
}
|
||||
|
||||
swaggerResource := SwaggerResource{
|
||||
Name: resource.Name,
|
||||
URLName: strings.ToLower(resource.Name), // TODO: Use dash separator
|
||||
Properties: properties,
|
||||
PayloadProperties: payloadProperties,
|
||||
}
|
||||
|
||||
return &swaggerResource
|
||||
}
|
||||
|
||||
const (
|
||||
propertyTypeString = "string"
|
||||
propertyTypeInteger = "integer"
|
||||
propertyTypeBoolean = "boolean"
|
||||
propertyTypeNumber = "number"
|
||||
propertyTypeDateTime = "date-time"
|
||||
propertyTypeUUID4 = "uuid4"
|
||||
)
|
||||
|
||||
func fieldToProperty(field Field) Property {
|
||||
propertyType := propertyTypeString // Fallback type
|
||||
required := true
|
||||
var format *string
|
||||
|
||||
switch field.Type.Name {
|
||||
case "int":
|
||||
propertyType = propertyTypeInteger
|
||||
case "null.Int":
|
||||
propertyType = propertyTypeInteger
|
||||
required = false
|
||||
case "bool":
|
||||
propertyType = propertyTypeBoolean
|
||||
case "null.Bool":
|
||||
propertyType = propertyTypeBoolean
|
||||
required = false
|
||||
case "null.String":
|
||||
propertyType = propertyTypeString
|
||||
required = false
|
||||
case "types.Decimal":
|
||||
propertyType = propertyTypeNumber
|
||||
case "types.NullDecimal":
|
||||
propertyType = propertyTypeNumber
|
||||
required = false
|
||||
case "time.Time":
|
||||
format = swag.String(propertyTypeDateTime)
|
||||
case "null.Time":
|
||||
format = swag.String(propertyTypeDateTime)
|
||||
required = false
|
||||
}
|
||||
|
||||
if strings.Contains(field.Name, "ID") {
|
||||
format = swag.String(propertyTypeUUID4)
|
||||
}
|
||||
|
||||
return Property{
|
||||
Name: goToJSNaming(field.Name),
|
||||
Type: propertyType,
|
||||
Required: required,
|
||||
Format: format,
|
||||
}
|
||||
}
|
||||
|
||||
type HandlerField struct {
|
||||
Name string
|
||||
Value string
|
||||
PlaceholderValue string
|
||||
}
|
||||
|
||||
type HandlerResource struct {
|
||||
Name string
|
||||
Fields []HandlerField
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Module string
|
||||
Package string
|
||||
Resource *HandlerResource
|
||||
}
|
||||
|
||||
func toHandlerResource(storageResource *StorageResource, swaggerResource *SwaggerResource) *HandlerResource {
|
||||
fields := make([]HandlerField, len(swaggerResource.Properties))
|
||||
for i, property := range swaggerResource.Properties {
|
||||
fields[i] = propertyToHandlerField(property)
|
||||
|
||||
// Hack to get the proper field name.
|
||||
fields[i].Name = storageResource.Fields[i].Name
|
||||
}
|
||||
|
||||
return &HandlerResource{
|
||||
Name: swaggerResource.Name,
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
func propertyToHandlerField(property Property) HandlerField {
|
||||
placeholderValue := `swag.String("` + property.Name + `")` // Fallback placeholder
|
||||
|
||||
switch property.Type {
|
||||
case "integer":
|
||||
placeholderValue = `swag.Int64(100)`
|
||||
case "boolean":
|
||||
placeholderValue = `swag.Bool(true)`
|
||||
case "number":
|
||||
placeholderValue = `swag.Float64(10.0)`
|
||||
}
|
||||
|
||||
if property.Format != nil {
|
||||
switch *property.Format {
|
||||
case "date-time":
|
||||
placeholderValue = `conv.DateTime(strfmt.DateTime(time.Now()))`
|
||||
|
||||
case "uuid4":
|
||||
placeholderValue = `conv.UUID4(strfmt.UUID4("1606388b-1f88-4f56-bd97-c27fbc3fe080"))`
|
||||
}
|
||||
}
|
||||
|
||||
return HandlerField{
|
||||
Name: property.Name,
|
||||
PlaceholderValue: placeholderValue,
|
||||
}
|
||||
}
|
||||
|
||||
type handlerConfig struct {
|
||||
filePrefix string
|
||||
fileSuffix string
|
||||
template string
|
||||
}
|
||||
|
||||
var configuredHandlers = map[string]handlerConfig{
|
||||
"get-all": {"get_", "_list.go", getListHandlerTemplate},
|
||||
"get": {"get_", ".go", getHandlerTemplate},
|
||||
"post": {"post_", ".go", postHandlerTemplate},
|
||||
"put": {"put_", ".go", putHandlerTemplate},
|
||||
"delete": {"delete_", ".go", deleteHandlerTemplate},
|
||||
}
|
||||
|
||||
func GenerateHandlers(resource *StorageResource, handlerBaseDir, modulePath string, methods []string, force bool) error {
|
||||
packageName := strings.ToLower(resource.Name)
|
||||
resourceBaseDir := filepath.Join(handlerBaseDir, packageName)
|
||||
|
||||
if _, err := os.Stat(resourceBaseDir); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(resourceBaseDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create resource base directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
module, err := getModuleName(modulePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := Handler{
|
||||
Module: module,
|
||||
Package: packageName,
|
||||
Resource: toHandlerResource(resource, toSwaggerResource(resource)),
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
handlerConfig, ok := configuredHandlers[method]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(resourceBaseDir, handlerConfig.filePrefix+packageName+handlerConfig.fileSuffix)
|
||||
if err := executeTemplate(handlerConfig.template, outputPath, handler, force); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func createDirIfAbsent(path string) error {
|
||||
if !fileExists(path) {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory '%s': %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeTemplate(templateStr, outputPath string, data interface{}, force bool) error {
|
||||
templ := template.Template{}
|
||||
if _, err := templ.Parse(templateStr); err != nil {
|
||||
return fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
|
||||
if !force && fileExists(outputPath) {
|
||||
return fmt.Errorf("file '%s' already exists; call with --force to overwrite", outputPath)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file '%s': %w", outputPath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := templ.Execute(file, data); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func goToJSNaming(name string) string {
|
||||
// Hack to properly handle ID.
|
||||
switch name {
|
||||
case "ID":
|
||||
return "id"
|
||||
default:
|
||||
first := strings.ToLower(name[0:1])
|
||||
return first + name[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func getModuleName(absolutePathToGoMod string) (string, error) {
|
||||
dat, err := os.ReadFile(absolutePathToGoMod)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read go.mod: %w", err)
|
||||
}
|
||||
|
||||
mod := modfile.ModulePath(dat)
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
86
scripts/internal/scaffold/scaffold_test.go
Normal file
86
scripts/internal/scaffold/scaffold_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//go:build scripts
|
||||
|
||||
package scaffold_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/scripts/internal/scaffold"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
resourcePath = "testdata/test_resource.txt"
|
||||
definitionsPath = "testdata/definitions"
|
||||
pathsPath = "testdata/paths"
|
||||
handlerPath = "testdata/testresource"
|
||||
modulePath = filepath.Join(util.GetProjectRootDir(), "go.mod")
|
||||
|
||||
defaultMethods = []string{"get-all", "get", "post", "put", "delete"}
|
||||
)
|
||||
|
||||
func TestParseModel_Success(t *testing.T) {
|
||||
// Execute
|
||||
resource, err := scaffold.ParseModel(resourcePath)
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
test.Snapshoter.Save(t, resource)
|
||||
}
|
||||
|
||||
func TestGenerateSwagger_Success(t *testing.T) {
|
||||
// Setup
|
||||
resource, err := scaffold.ParseModel(resourcePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Execute
|
||||
err = scaffold.GenerateSwagger(resource, "testdata", true)
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.DirExists(t, definitionsPath, "Should create the definitions directory")
|
||||
assert.DirExists(t, pathsPath, "Should create the paths directory")
|
||||
assert.FileExists(t, filepath.Join(definitionsPath, "testresource.yml"), "Should generate the definition spec")
|
||||
assert.FileExists(t, filepath.Join(pathsPath, "testresource.yml"), "Should generate the paths spec")
|
||||
|
||||
// Cleanup
|
||||
err = os.RemoveAll(definitionsPath)
|
||||
require.NoError(t, err)
|
||||
err = os.RemoveAll(pathsPath)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateHandlers_Success(t *testing.T) {
|
||||
// Setup
|
||||
resource, err := scaffold.ParseModel(resourcePath)
|
||||
require.NoError(t, err)
|
||||
err = scaffold.GenerateSwagger(resource, "testdata", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Execute
|
||||
err = scaffold.GenerateHandlers(resource, "testdata", modulePath, defaultMethods, true)
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.DirExists(t, handlerPath, "Should create the handler directory")
|
||||
assert.FileExists(t, filepath.Join(handlerPath, "get_testresource_list.go"), "Should create the GET list handler")
|
||||
assert.FileExists(t, filepath.Join(handlerPath, "get_testresource.go"), "Should create the GET handler")
|
||||
assert.FileExists(t, filepath.Join(handlerPath, "post_testresource.go"), "Should create the POST handler")
|
||||
assert.FileExists(t, filepath.Join(handlerPath, "put_testresource.go"), "Should create the PUT handler")
|
||||
assert.FileExists(t, filepath.Join(handlerPath, "delete_testresource.go"), "Should create the DELETE handler")
|
||||
|
||||
// Cleanup
|
||||
err = os.RemoveAll(definitionsPath)
|
||||
require.NoError(t, err)
|
||||
err = os.RemoveAll(pathsPath)
|
||||
require.NoError(t, err)
|
||||
err = os.RemoveAll(handlerPath)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
366
scripts/internal/scaffold/templates.go
Normal file
366
scripts/internal/scaffold/templates.go
Normal file
@@ -0,0 +1,366 @@
|
||||
//go:build scripts
|
||||
|
||||
package scaffold
|
||||
|
||||
const (
|
||||
swaggerDefinitionsTemplate = `swagger: "2.0"
|
||||
info:
|
||||
title: ""
|
||||
version: 0.1.0
|
||||
paths: {}
|
||||
definitions:
|
||||
{{ .Name }}:
|
||||
type: object
|
||||
required:
|
||||
{{- range .Properties}}
|
||||
{{- if .Required }}
|
||||
- {{ .Name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
properties:
|
||||
{{- range .Properties}}
|
||||
{{ .Name }}:
|
||||
type: {{ .Type }}
|
||||
{{- if not .Required }}
|
||||
x-nullable: true
|
||||
{{- end }}
|
||||
{{- if .Format }}
|
||||
format: {{ .Format }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{ .Name }}Payload:
|
||||
type: object
|
||||
required:
|
||||
{{- range .PayloadProperties}}
|
||||
{{- if .Required }}
|
||||
- {{ .Name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
properties:
|
||||
{{- range .PayloadProperties}}
|
||||
{{ .Name }}:
|
||||
type: {{ .Type }}
|
||||
{{- if not .Required }}
|
||||
x-nullable: true
|
||||
{{- end }}
|
||||
{{- if .Format }}
|
||||
format: {{ .Format }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{ .Name }}List:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/{{ .Name }}"
|
||||
`
|
||||
|
||||
swaggerPathsTemplate = `swagger: "2.0"
|
||||
info:
|
||||
title: ""
|
||||
version: 0.1.0
|
||||
parameters:
|
||||
{{ .Name }}IdParam:
|
||||
type: string
|
||||
format: uuid4
|
||||
name: id
|
||||
description: ID of {{ .Name }}
|
||||
in: path
|
||||
required: true
|
||||
|
||||
paths:
|
||||
/api/v1/{{ .URLName }}s:
|
||||
get:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: "Return a list of {{ .Name }}"
|
||||
tags:
|
||||
- {{ .Name }}
|
||||
summary: "Return a list of {{ .Name }}"
|
||||
operationId: Get{{ .Name }}ListRoute
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}List"
|
||||
|
||||
post:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: "Update the given {{ .Name }}"
|
||||
tags:
|
||||
- {{ .Name }}
|
||||
summary: "Update the given {{ .Name }}"
|
||||
operationId: Post{{ .Name }}Route
|
||||
parameters:
|
||||
- name: Payload
|
||||
in: body
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}Payload"
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}"
|
||||
|
||||
/api/v1/{{ .URLName }}s/{id}:
|
||||
get:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: "Return {{ .Name }} with ID"
|
||||
tags:
|
||||
- {{ .Name }}
|
||||
summary: "Return {{ .Name }} with ID"
|
||||
operationId: Get{{ .Name }}Route
|
||||
parameters:
|
||||
- $ref: "#/parameters/{{ .Name }}IdParam"
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}"
|
||||
|
||||
put:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: "Update the given {{ .Name }}"
|
||||
tags:
|
||||
- {{ .Name }}
|
||||
summary: "Update the given {{ .Name }}"
|
||||
operationId: Put{{ .Name }}Route
|
||||
parameters:
|
||||
- $ref: "#/parameters/{{ .Name }}IdParam"
|
||||
- name: Payload
|
||||
in: body
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}Payload"
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
schema:
|
||||
$ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}"
|
||||
|
||||
delete:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: "Delete {{ .Name }} with ID"
|
||||
tags:
|
||||
- {{ .Name }}
|
||||
summary: "Delete {{ .Name }} with ID"
|
||||
operationId: Delete{{ .Name }}Route
|
||||
parameters:
|
||||
- $ref: "#/parameters/{{ .Name }}IdParam"
|
||||
responses:
|
||||
"204":
|
||||
description: Success
|
||||
`
|
||||
|
||||
getHandlerTemplate = `package {{ .Package }}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"{{ .Module }}/internal/api"
|
||||
"{{ .Module }}/internal/types"
|
||||
"{{ .Module }}/internal/util"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/strfmt/conv"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Get{{ .Resource.Name }}Route(s *api.Server) *echo.Route {
|
||||
return s.Router.APIV1{{ .Resource.Name }}.GET("/:id", get{{ .Resource.Name }}Handler(s))
|
||||
}
|
||||
|
||||
func get{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
/* Uncomment for real implementation
|
||||
ctx := c.Request().Context()
|
||||
|
||||
params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams()
|
||||
err := util.BindAndValidatePathParams(c, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := params.ID.String()
|
||||
|
||||
// TODO: Implement
|
||||
*/
|
||||
|
||||
response := types.{{ .Resource.Name }}{
|
||||
{{- range .Resource.Fields }}
|
||||
{{ .Name }}: {{ .PlaceholderValue }},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
return util.ValidateAndReturn(c, http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
getListHandlerTemplate = `package {{ .Package }}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"{{ .Module }}/internal/api"
|
||||
"{{ .Module }}/internal/types"
|
||||
"{{ .Module }}/internal/util"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/strfmt/conv"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Get{{ .Resource.Name }}ListRoute(s *api.Server) *echo.Route {
|
||||
return s.Router.APIV1{{ .Resource.Name }}.GET("", get{{ .Resource.Name }}ListHandler(s))
|
||||
}
|
||||
|
||||
func get{{ .Resource.Name }}ListHandler(s *api.Server) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
/* Uncomment for real implementation
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// TODO: Implement
|
||||
*/
|
||||
|
||||
item := types.{{ .Resource.Name }}{
|
||||
{{- range .Resource.Fields }}
|
||||
{{ .Name }}: {{ .PlaceholderValue }},
|
||||
{{- end }}
|
||||
}
|
||||
response := types.{{ .Resource.Name }}List{&item}
|
||||
|
||||
return util.ValidateAndReturn(c, http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
`
|
||||
postHandlerTemplate = `package {{ .Package }}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"{{ .Module }}/internal/api"
|
||||
"{{ .Module }}/internal/types"
|
||||
"{{ .Module }}/internal/util"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/strfmt/conv"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Post{{ .Resource.Name }}Route(s *api.Server) *echo.Route {
|
||||
return s.Router.APIV1{{ .Resource.Name }}.POST("", post{{ .Resource.Name }}Handler(s))
|
||||
}
|
||||
|
||||
func post{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
/* Uncomment for real implementation
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var body types.{{ .Resource.Name }}Payload
|
||||
err := util.BindAndValidateBody(c, &body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Implement
|
||||
*/
|
||||
|
||||
response := types.{{ .Resource.Name }}{
|
||||
{{- range .Resource.Fields }}
|
||||
{{ .Name }}: {{ .PlaceholderValue }},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
return util.ValidateAndReturn(c, http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
`
|
||||
putHandlerTemplate = `package {{ .Package }}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"{{ .Module }}/internal/api"
|
||||
"{{ .Module }}/internal/types"
|
||||
"{{ .Module }}/internal/util"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/strfmt/conv"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Put{{ .Resource.Name }}Route(s *api.Server) *echo.Route {
|
||||
return s.Router.APIV1{{ .Resource.Name }}.PUT("/:id", put{{ .Resource.Name }}Handler(s))
|
||||
}
|
||||
|
||||
func put{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
/* Uncomment for real implementation
|
||||
ctx := c.Request().Context()
|
||||
|
||||
params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams()
|
||||
err := util.BindAndValidatePathParams(c, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := params.ID.String()
|
||||
|
||||
var body types.{{ .Resource.Name }}Payload
|
||||
err = util.BindAndValidateBody(c, &body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Implement
|
||||
*/
|
||||
|
||||
response := types.{{ .Resource.Name }}{
|
||||
{{- range .Resource.Fields }}
|
||||
{{ .Name }}: {{ .PlaceholderValue }},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
return util.ValidateAndReturn(c, http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
`
|
||||
deleteHandlerTemplate = `package {{ .Package }}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"{{ .Module }}/internal/api"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Delete{{ .Resource.Name }}Route(s *api.Server) *echo.Route {
|
||||
return s.Router.APIV1{{ .Resource.Name }}.DELETE("/:id", delete{{ .Resource.Name }}Handler(s))
|
||||
}
|
||||
|
||||
func delete{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
/* Uncomment for real implementation
|
||||
ctx := c.Request().Context()
|
||||
|
||||
params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams()
|
||||
err := util.BindAndValidatePathParams(c, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := params.ID.String()
|
||||
|
||||
// TODO: Implement
|
||||
*/
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
758
scripts/internal/scaffold/testdata/test_resource.txt
vendored
Normal file
758
scripts/internal/scaffold/testdata/test_resource.txt
vendored
Normal file
@@ -0,0 +1,758 @@
|
||||
// Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
|
||||
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/friendsofgo/errors"
|
||||
"github.com/volatiletech/null/v8"
|
||||
"github.com/volatiletech/sqlboiler/v4/boil"
|
||||
"github.com/volatiletech/sqlboiler/v4/queries"
|
||||
"github.com/volatiletech/sqlboiler/v4/queries/qm"
|
||||
"github.com/volatiletech/sqlboiler/v4/queries/qmhelper"
|
||||
"github.com/volatiletech/sqlboiler/v4/types"
|
||||
"github.com/volatiletech/strmangle"
|
||||
)
|
||||
|
||||
// TestResource is an object representing the database table.
|
||||
type TestResource struct {
|
||||
ID string `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||
NumericField types.Decimal `boil:"numeric_field" json:"numeric_field" toml:"numeric_field" yaml:"numeric_field"`
|
||||
NumericNullField types.NullDecimal `boil:"numeric_null_field" json:"numeric_null_field,omitempty" toml:"numeric_null_field" yaml:"numeric_null_field,omitempty"`
|
||||
IntegerField int `boil:"integer_field" json:"integer_field" toml:"integer_field" yaml:"integer_field"`
|
||||
IntegerNullField null.Int `boil:"integer_null_field" json:"integer_null_field,omitempty" toml:"integer_null_field" yaml:"integer_null_field,omitempty"`
|
||||
BoolField bool `boil:"bool_field" json:"bool_field" toml:"bool_field" yaml:"bool_field"`
|
||||
BoolNullField null.Bool `boil:"bool_null_field" json:"bool_null_field,omitempty" toml:"bool_null_field" yaml:"bool_null_field,omitempty"`
|
||||
DecimalField types.Decimal `boil:"decimal_field" json:"decimal_field" toml:"decimal_field" yaml:"decimal_field"`
|
||||
DecimalNullField types.NullDecimal `boil:"decimal_null_field" json:"decimal_null_field,omitempty" toml:"decimal_null_field" yaml:"decimal_null_field,omitempty"`
|
||||
TextField string `boil:"text_field" json:"text_field" toml:"text_field" yaml:"text_field"`
|
||||
TextNullField null.String `boil:"text_null_field" json:"text_null_field,omitempty" toml:"text_null_field" yaml:"text_null_field,omitempty"`
|
||||
TimtestamptzNullField null.Time `boil:"timtestamptz_null_field" json:"timtestamptz_null_field,omitempty" toml:"timtestamptz_null_field" yaml:"timtestamptz_null_field,omitempty"`
|
||||
CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"`
|
||||
UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"`
|
||||
|
||||
R *testResourceR `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
L testResourceL `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
var TestResourceColumns = struct {
|
||||
ID string
|
||||
NumericField string
|
||||
NumericNullField string
|
||||
IntegerField string
|
||||
IntegerNullField string
|
||||
BoolField string
|
||||
BoolNullField string
|
||||
DecimalField string
|
||||
DecimalNullField string
|
||||
TextField string
|
||||
TextNullField string
|
||||
TimtestamptzNullField string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}{
|
||||
ID: "id",
|
||||
NumericField: "numeric_field",
|
||||
NumericNullField: "numeric_null_field",
|
||||
IntegerField: "integer_field",
|
||||
IntegerNullField: "integer_null_field",
|
||||
BoolField: "bool_field",
|
||||
BoolNullField: "bool_null_field",
|
||||
DecimalField: "decimal_field",
|
||||
DecimalNullField: "decimal_null_field",
|
||||
TextField: "text_field",
|
||||
TextNullField: "text_null_field",
|
||||
TimtestamptzNullField: "timtestamptz_null_field",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
}
|
||||
|
||||
// Generated where
|
||||
|
||||
type whereHelpernull_Bool struct{ field string }
|
||||
|
||||
func (w whereHelpernull_Bool) EQ(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.WhereNullEQ(w.field, false, x)
|
||||
}
|
||||
func (w whereHelpernull_Bool) NEQ(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.WhereNullEQ(w.field, true, x)
|
||||
}
|
||||
func (w whereHelpernull_Bool) IsNull() qm.QueryMod { return qmhelper.WhereIsNull(w.field) }
|
||||
func (w whereHelpernull_Bool) IsNotNull() qm.QueryMod { return qmhelper.WhereIsNotNull(w.field) }
|
||||
func (w whereHelpernull_Bool) LT(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.LT, x)
|
||||
}
|
||||
func (w whereHelpernull_Bool) LTE(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.LTE, x)
|
||||
}
|
||||
func (w whereHelpernull_Bool) GT(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.GT, x)
|
||||
}
|
||||
func (w whereHelpernull_Bool) GTE(x null.Bool) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.GTE, x)
|
||||
}
|
||||
|
||||
var TestResourceWhere = struct {
|
||||
ID whereHelperstring
|
||||
NumericField whereHelpertypes_Decimal
|
||||
NumericNullField whereHelpertypes_NullDecimal
|
||||
IntegerField whereHelperint
|
||||
IntegerNullField whereHelpernull_Int
|
||||
BoolField whereHelperbool
|
||||
BoolNullField whereHelpernull_Bool
|
||||
DecimalField whereHelpertypes_Decimal
|
||||
DecimalNullField whereHelpertypes_NullDecimal
|
||||
TextField whereHelperstring
|
||||
TextNullField whereHelpernull_String
|
||||
TimtestamptzNullField whereHelpernull_Time
|
||||
CreatedAt whereHelpertime_Time
|
||||
UpdatedAt whereHelpertime_Time
|
||||
}{
|
||||
ID: whereHelperstring{field: "\"test_resource\".\"id\""},
|
||||
NumericField: whereHelpertypes_Decimal{field: "\"test_resource\".\"numeric_field\""},
|
||||
NumericNullField: whereHelpertypes_NullDecimal{field: "\"test_resource\".\"numeric_null_field\""},
|
||||
IntegerField: whereHelperint{field: "\"test_resource\".\"integer_field\""},
|
||||
IntegerNullField: whereHelpernull_Int{field: "\"test_resource\".\"integer_null_field\""},
|
||||
BoolField: whereHelperbool{field: "\"test_resource\".\"bool_field\""},
|
||||
BoolNullField: whereHelpernull_Bool{field: "\"test_resource\".\"bool_null_field\""},
|
||||
DecimalField: whereHelpertypes_Decimal{field: "\"test_resource\".\"decimal_field\""},
|
||||
DecimalNullField: whereHelpertypes_NullDecimal{field: "\"test_resource\".\"decimal_null_field\""},
|
||||
TextField: whereHelperstring{field: "\"test_resource\".\"text_field\""},
|
||||
TextNullField: whereHelpernull_String{field: "\"test_resource\".\"text_null_field\""},
|
||||
TimtestamptzNullField: whereHelpernull_Time{field: "\"test_resource\".\"timtestamptz_null_field\""},
|
||||
CreatedAt: whereHelpertime_Time{field: "\"test_resource\".\"created_at\""},
|
||||
UpdatedAt: whereHelpertime_Time{field: "\"test_resource\".\"updated_at\""},
|
||||
}
|
||||
|
||||
// TestResourceRels is where relationship names are stored.
|
||||
var TestResourceRels = struct {
|
||||
}{}
|
||||
|
||||
// testResourceR is where relationships are stored.
|
||||
type testResourceR struct {
|
||||
}
|
||||
|
||||
// NewStruct creates a new relationship struct
|
||||
func (*testResourceR) NewStruct() *testResourceR {
|
||||
return &testResourceR{}
|
||||
}
|
||||
|
||||
// testResourceL is where Load methods for each relationship are stored.
|
||||
type testResourceL struct{}
|
||||
|
||||
var (
|
||||
testResourceAllColumns = []string{"id", "numeric_field", "numeric_null_field", "integer_field", "integer_null_field", "bool_field", "bool_null_field", "decimal_field", "decimal_null_field", "text_field", "text_null_field", "timtestamptz_null_field", "created_at", "updated_at"}
|
||||
testResourceColumnsWithoutDefault = []string{"id", "numeric_field", "numeric_null_field", "integer_field", "integer_null_field", "bool_field", "bool_null_field", "decimal_field", "decimal_null_field", "text_field", "text_null_field", "timtestamptz_null_field", "created_at", "updated_at"}
|
||||
testResourceColumnsWithDefault = []string{}
|
||||
testResourcePrimaryKeyColumns = []string{"id"}
|
||||
)
|
||||
|
||||
type (
|
||||
// TestResourceSlice is an alias for a slice of pointers to TestResource.
|
||||
// This should generally be used opposed to []TestResource.
|
||||
TestResourceSlice []*TestResource
|
||||
|
||||
testResourceQuery struct {
|
||||
*queries.Query
|
||||
}
|
||||
)
|
||||
|
||||
// Cache for insert, update and upsert
|
||||
var (
|
||||
testResourceType = reflect.TypeOf(&TestResource{})
|
||||
testResourceMapping = queries.MakeStructMapping(testResourceType)
|
||||
testResourcePrimaryKeyMapping, _ = queries.BindMapping(testResourceType, testResourceMapping, testResourcePrimaryKeyColumns)
|
||||
testResourceInsertCacheMut sync.RWMutex
|
||||
testResourceInsertCache = make(map[string]insertCache)
|
||||
testResourceUpdateCacheMut sync.RWMutex
|
||||
testResourceUpdateCache = make(map[string]updateCache)
|
||||
testResourceUpsertCacheMut sync.RWMutex
|
||||
testResourceUpsertCache = make(map[string]insertCache)
|
||||
)
|
||||
|
||||
var (
|
||||
// Force time package dependency for automated UpdatedAt/CreatedAt.
|
||||
_ = time.Second
|
||||
// Force qmhelper dependency for where clause generation (which doesn't
|
||||
// always happen)
|
||||
_ = qmhelper.Where
|
||||
)
|
||||
|
||||
// One returns a single testResource record from the query.
|
||||
func (q testResourceQuery) One(ctx context.Context, exec boil.ContextExecutor) (*TestResource, error) {
|
||||
o := &TestResource{}
|
||||
|
||||
queries.SetLimit(q.Query, 1)
|
||||
|
||||
err := q.Bind(ctx, exec, o)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == sql.ErrNoRows {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return nil, errors.Wrap(err, "models: failed to execute a one query for test_resource")
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// All returns all TestResource records from the query.
|
||||
func (q testResourceQuery) All(ctx context.Context, exec boil.ContextExecutor) (TestResourceSlice, error) {
|
||||
var o []*TestResource
|
||||
|
||||
err := q.Bind(ctx, exec, &o)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "models: failed to assign all query results to TestResource slice")
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Count returns the count of all TestResource records in the query.
|
||||
func (q testResourceQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
|
||||
var count int64
|
||||
|
||||
queries.SetSelect(q.Query, nil)
|
||||
queries.SetCount(q.Query)
|
||||
|
||||
err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: failed to count test_resource rows")
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Exists checks if the row exists in the table.
|
||||
func (q testResourceQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) {
|
||||
var count int64
|
||||
|
||||
queries.SetSelect(q.Query, nil)
|
||||
queries.SetCount(q.Query)
|
||||
queries.SetLimit(q.Query, 1)
|
||||
|
||||
err := q.Query.QueryRowContext(ctx, exec).Scan(&count)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "models: failed to check if test_resource exists")
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// TestResources retrieves all the records using an executor.
|
||||
func TestResources(mods ...qm.QueryMod) testResourceQuery {
|
||||
mods = append(mods, qm.From("\"test_resource\""))
|
||||
return testResourceQuery{NewQuery(mods...)}
|
||||
}
|
||||
|
||||
// FindTestResource retrieves a single record by ID with an executor.
|
||||
// If selectCols is empty Find will return all columns.
|
||||
func FindTestResource(ctx context.Context, exec boil.ContextExecutor, iD string, selectCols ...string) (*TestResource, error) {
|
||||
testResourceObj := &TestResource{}
|
||||
|
||||
sel := "*"
|
||||
if len(selectCols) > 0 {
|
||||
sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",")
|
||||
}
|
||||
query := fmt.Sprintf(
|
||||
"select %s from \"test_resource\" where \"id\"=$1", sel,
|
||||
)
|
||||
|
||||
q := queries.Raw(query, iD)
|
||||
|
||||
err := q.Bind(ctx, exec, testResourceObj)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == sql.ErrNoRows {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return nil, errors.Wrap(err, "models: unable to select from test_resource")
|
||||
}
|
||||
|
||||
return testResourceObj, nil
|
||||
}
|
||||
|
||||
// Insert a single record using an executor.
|
||||
// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts.
|
||||
func (o *TestResource) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
|
||||
if o == nil {
|
||||
return errors.New("models: no test_resource provided for insertion")
|
||||
}
|
||||
|
||||
var err error
|
||||
if !boil.TimestampsAreSkipped(ctx) {
|
||||
currTime := time.Now().In(boil.GetLocation())
|
||||
|
||||
if o.CreatedAt.IsZero() {
|
||||
o.CreatedAt = currTime
|
||||
}
|
||||
if o.UpdatedAt.IsZero() {
|
||||
o.UpdatedAt = currTime
|
||||
}
|
||||
}
|
||||
|
||||
nzDefaults := queries.NonZeroDefaultSet(testResourceColumnsWithDefault, o)
|
||||
|
||||
key := makeCacheKey(columns, nzDefaults)
|
||||
testResourceInsertCacheMut.RLock()
|
||||
cache, cached := testResourceInsertCache[key]
|
||||
testResourceInsertCacheMut.RUnlock()
|
||||
|
||||
if !cached {
|
||||
wl, returnColumns := columns.InsertColumnSet(
|
||||
testResourceAllColumns,
|
||||
testResourceColumnsWithDefault,
|
||||
testResourceColumnsWithoutDefault,
|
||||
nzDefaults,
|
||||
)
|
||||
|
||||
cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, wl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache.retMapping, err = queries.BindMapping(testResourceType, testResourceMapping, returnColumns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(wl) != 0 {
|
||||
cache.query = fmt.Sprintf("INSERT INTO \"test_resource\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1))
|
||||
} else {
|
||||
cache.query = "INSERT INTO \"test_resource\" %sDEFAULT VALUES%s"
|
||||
}
|
||||
|
||||
var queryOutput, queryReturning string
|
||||
|
||||
if len(cache.retMapping) != 0 {
|
||||
queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\""))
|
||||
}
|
||||
|
||||
cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning)
|
||||
}
|
||||
|
||||
value := reflect.Indirect(reflect.ValueOf(o))
|
||||
vals := queries.ValuesFromMapping(value, cache.valueMapping)
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, cache.query)
|
||||
fmt.Fprintln(writer, vals)
|
||||
}
|
||||
|
||||
if len(cache.retMapping) != 0 {
|
||||
err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...)
|
||||
} else {
|
||||
_, err = exec.ExecContext(ctx, cache.query, vals...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "models: unable to insert into test_resource")
|
||||
}
|
||||
|
||||
if !cached {
|
||||
testResourceInsertCacheMut.Lock()
|
||||
testResourceInsertCache[key] = cache
|
||||
testResourceInsertCacheMut.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update uses an executor to update the TestResource.
|
||||
// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates.
|
||||
// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records.
|
||||
func (o *TestResource) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) {
|
||||
if !boil.TimestampsAreSkipped(ctx) {
|
||||
currTime := time.Now().In(boil.GetLocation())
|
||||
|
||||
o.UpdatedAt = currTime
|
||||
}
|
||||
|
||||
var err error
|
||||
key := makeCacheKey(columns, nil)
|
||||
testResourceUpdateCacheMut.RLock()
|
||||
cache, cached := testResourceUpdateCache[key]
|
||||
testResourceUpdateCacheMut.RUnlock()
|
||||
|
||||
if !cached {
|
||||
wl := columns.UpdateColumnSet(
|
||||
testResourceAllColumns,
|
||||
testResourcePrimaryKeyColumns,
|
||||
)
|
||||
|
||||
if !columns.IsWhitelist() {
|
||||
wl = strmangle.SetComplement(wl, []string{"created_at"})
|
||||
}
|
||||
if len(wl) == 0 {
|
||||
return 0, errors.New("models: unable to update test_resource, could not build whitelist")
|
||||
}
|
||||
|
||||
cache.query = fmt.Sprintf("UPDATE \"test_resource\" SET %s WHERE %s",
|
||||
strmangle.SetParamNames("\"", "\"", 1, wl),
|
||||
strmangle.WhereClause("\"", "\"", len(wl)+1, testResourcePrimaryKeyColumns),
|
||||
)
|
||||
cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, append(wl, testResourcePrimaryKeyColumns...))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping)
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, cache.query)
|
||||
fmt.Fprintln(writer, values)
|
||||
}
|
||||
var result sql.Result
|
||||
result, err = exec.ExecContext(ctx, cache.query, values...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to update test_resource row")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: failed to get rows affected by update for test_resource")
|
||||
}
|
||||
|
||||
if !cached {
|
||||
testResourceUpdateCacheMut.Lock()
|
||||
testResourceUpdateCache[key] = cache
|
||||
testResourceUpdateCacheMut.Unlock()
|
||||
}
|
||||
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// UpdateAll updates all rows with the specified column values.
|
||||
func (q testResourceQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
|
||||
queries.SetUpdate(q.Query, cols)
|
||||
|
||||
result, err := q.Query.ExecContext(ctx, exec)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to update all for test_resource")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to retrieve rows affected for test_resource")
|
||||
}
|
||||
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// UpdateAll updates all rows with the specified column values, using an executor.
|
||||
func (o TestResourceSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) {
|
||||
ln := int64(len(o))
|
||||
if ln == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if len(cols) == 0 {
|
||||
return 0, errors.New("models: update all requires at least one column argument")
|
||||
}
|
||||
|
||||
colNames := make([]string, len(cols))
|
||||
args := make([]interface{}, len(cols))
|
||||
|
||||
i := 0
|
||||
for name, value := range cols {
|
||||
colNames[i] = name
|
||||
args[i] = value
|
||||
i++
|
||||
}
|
||||
|
||||
// Append all of the primary key values for each column
|
||||
for _, obj := range o {
|
||||
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping)
|
||||
args = append(args, pkeyArgs...)
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf("UPDATE \"test_resource\" SET %s WHERE %s",
|
||||
strmangle.SetParamNames("\"", "\"", 1, colNames),
|
||||
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, testResourcePrimaryKeyColumns, len(o)))
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, sql)
|
||||
fmt.Fprintln(writer, args...)
|
||||
}
|
||||
result, err := exec.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to update all in testResource slice")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all testResource")
|
||||
}
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// Upsert attempts an insert using an executor, and does an update or ignore on conflict.
|
||||
// See boil.Columns documentation for how to properly use updateColumns and insertColumns.
|
||||
func (o *TestResource) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error {
|
||||
if o == nil {
|
||||
return errors.New("models: no test_resource provided for upsert")
|
||||
}
|
||||
if !boil.TimestampsAreSkipped(ctx) {
|
||||
currTime := time.Now().In(boil.GetLocation())
|
||||
|
||||
if o.CreatedAt.IsZero() {
|
||||
o.CreatedAt = currTime
|
||||
}
|
||||
o.UpdatedAt = currTime
|
||||
}
|
||||
|
||||
nzDefaults := queries.NonZeroDefaultSet(testResourceColumnsWithDefault, o)
|
||||
|
||||
// Build cache key in-line uglily - mysql vs psql problems
|
||||
buf := strmangle.GetBuffer()
|
||||
if updateOnConflict {
|
||||
buf.WriteByte('t')
|
||||
} else {
|
||||
buf.WriteByte('f')
|
||||
}
|
||||
buf.WriteByte('.')
|
||||
for _, c := range conflictColumns {
|
||||
buf.WriteString(c)
|
||||
}
|
||||
buf.WriteByte('.')
|
||||
buf.WriteString(strconv.Itoa(updateColumns.Kind))
|
||||
for _, c := range updateColumns.Cols {
|
||||
buf.WriteString(c)
|
||||
}
|
||||
buf.WriteByte('.')
|
||||
buf.WriteString(strconv.Itoa(insertColumns.Kind))
|
||||
for _, c := range insertColumns.Cols {
|
||||
buf.WriteString(c)
|
||||
}
|
||||
buf.WriteByte('.')
|
||||
for _, c := range nzDefaults {
|
||||
buf.WriteString(c)
|
||||
}
|
||||
key := buf.String()
|
||||
strmangle.PutBuffer(buf)
|
||||
|
||||
testResourceUpsertCacheMut.RLock()
|
||||
cache, cached := testResourceUpsertCache[key]
|
||||
testResourceUpsertCacheMut.RUnlock()
|
||||
|
||||
var err error
|
||||
|
||||
if !cached {
|
||||
insert, ret := insertColumns.InsertColumnSet(
|
||||
testResourceAllColumns,
|
||||
testResourceColumnsWithDefault,
|
||||
testResourceColumnsWithoutDefault,
|
||||
nzDefaults,
|
||||
)
|
||||
update := updateColumns.UpdateColumnSet(
|
||||
testResourceAllColumns,
|
||||
testResourcePrimaryKeyColumns,
|
||||
)
|
||||
|
||||
if updateOnConflict && len(update) == 0 {
|
||||
return errors.New("models: unable to upsert test_resource, could not build update column list")
|
||||
}
|
||||
|
||||
conflict := conflictColumns
|
||||
if len(conflict) == 0 {
|
||||
conflict = make([]string, len(testResourcePrimaryKeyColumns))
|
||||
copy(conflict, testResourcePrimaryKeyColumns)
|
||||
}
|
||||
cache.query = buildUpsertQueryPostgres(dialect, "\"test_resource\"", updateOnConflict, ret, update, conflict, insert)
|
||||
|
||||
cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, insert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ret) != 0 {
|
||||
cache.retMapping, err = queries.BindMapping(testResourceType, testResourceMapping, ret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value := reflect.Indirect(reflect.ValueOf(o))
|
||||
vals := queries.ValuesFromMapping(value, cache.valueMapping)
|
||||
var returns []interface{}
|
||||
if len(cache.retMapping) != 0 {
|
||||
returns = queries.PtrsFromMapping(value, cache.retMapping)
|
||||
}
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, cache.query)
|
||||
fmt.Fprintln(writer, vals)
|
||||
}
|
||||
if len(cache.retMapping) != 0 {
|
||||
err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...)
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil // Postgres doesn't return anything when there's no update
|
||||
}
|
||||
} else {
|
||||
_, err = exec.ExecContext(ctx, cache.query, vals...)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "models: unable to upsert test_resource")
|
||||
}
|
||||
|
||||
if !cached {
|
||||
testResourceUpsertCacheMut.Lock()
|
||||
testResourceUpsertCache[key] = cache
|
||||
testResourceUpsertCacheMut.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a single TestResource record with an executor.
|
||||
// Delete will match against the primary key column to find the record to delete.
|
||||
func (o *TestResource) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
|
||||
if o == nil {
|
||||
return 0, errors.New("models: no TestResource provided for delete")
|
||||
}
|
||||
|
||||
args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), testResourcePrimaryKeyMapping)
|
||||
sql := "DELETE FROM \"test_resource\" WHERE \"id\"=$1"
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, sql)
|
||||
fmt.Fprintln(writer, args...)
|
||||
}
|
||||
result, err := exec.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to delete from test_resource")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: failed to get rows affected by delete for test_resource")
|
||||
}
|
||||
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// DeleteAll deletes all matching rows.
|
||||
func (q testResourceQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
|
||||
if q.Query == nil {
|
||||
return 0, errors.New("models: no testResourceQuery provided for delete all")
|
||||
}
|
||||
|
||||
queries.SetDelete(q.Query)
|
||||
|
||||
result, err := q.Query.ExecContext(ctx, exec)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to delete all from test_resource")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for test_resource")
|
||||
}
|
||||
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// DeleteAll deletes all rows in the slice, using an executor.
|
||||
func (o TestResourceSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
|
||||
if len(o) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, obj := range o {
|
||||
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping)
|
||||
args = append(args, pkeyArgs...)
|
||||
}
|
||||
|
||||
sql := "DELETE FROM \"test_resource\" WHERE " +
|
||||
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, testResourcePrimaryKeyColumns, len(o))
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, sql)
|
||||
fmt.Fprintln(writer, args)
|
||||
}
|
||||
result, err := exec.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: unable to delete all from testResource slice")
|
||||
}
|
||||
|
||||
rowsAff, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for test_resource")
|
||||
}
|
||||
|
||||
return rowsAff, nil
|
||||
}
|
||||
|
||||
// Reload refetches the object from the database
|
||||
// using the primary keys with an executor.
|
||||
func (o *TestResource) Reload(ctx context.Context, exec boil.ContextExecutor) error {
|
||||
ret, err := FindTestResource(ctx, exec, o.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = *ret
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadAll refetches every row with matching primary key column values
|
||||
// and overwrites the original object slice with the newly updated slice.
|
||||
func (o *TestResourceSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error {
|
||||
if o == nil || len(*o) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
slice := TestResourceSlice{}
|
||||
var args []interface{}
|
||||
for _, obj := range *o {
|
||||
pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping)
|
||||
args = append(args, pkeyArgs...)
|
||||
}
|
||||
|
||||
sql := "SELECT \"test_resource\".* FROM \"test_resource\" WHERE " +
|
||||
strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, testResourcePrimaryKeyColumns, len(*o))
|
||||
|
||||
q := queries.Raw(sql, args...)
|
||||
|
||||
err := q.Bind(ctx, exec, &slice)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "models: unable to reload all in TestResourceSlice")
|
||||
}
|
||||
|
||||
*o = slice
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestResourceExists checks if the TestResource row exists.
|
||||
func TestResourceExists(ctx context.Context, exec boil.ContextExecutor, iD string) (bool, error) {
|
||||
var exists bool
|
||||
sql := "select exists(select 1 from \"test_resource\" where \"id\"=$1 limit 1)"
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, sql)
|
||||
fmt.Fprintln(writer, iD)
|
||||
}
|
||||
row := exec.QueryRowContext(ctx, sql, iD)
|
||||
|
||||
err := row.Scan(&exists)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "models: unable to check if test_resource exists")
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
28
scripts/internal/util/get_module_name.go
Normal file
28
scripts/internal/util/get_module_name.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build scripts
|
||||
|
||||
// nolint:revive
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
// GetModuleName returns the current go module's name as defined in the go.mod file.
|
||||
// https://stackoverflow.com/questions/53183356/api-to-get-the-module-name
|
||||
// https://github.com/rogpeppe/go-internal
|
||||
func GetModuleName(absolutePathToGoMod string) (string, error) {
|
||||
dat, err := os.ReadFile(absolutePathToGoMod)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return "", fmt.Errorf("failed to read go.mod file: %w", err)
|
||||
}
|
||||
|
||||
mod := modfile.ModulePath(dat)
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
14
scripts/internal/util/get_project_root_dir.go
Normal file
14
scripts/internal/util/get_project_root_dir.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build scripts
|
||||
|
||||
// nolint:revive
|
||||
package util
|
||||
|
||||
import "os"
|
||||
|
||||
func GetProjectRootDir() string {
|
||||
if val, ok := os.LookupEnv("PROJECT_ROOT_DIR"); ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return "/app"
|
||||
}
|
||||
9
scripts/main.go
Normal file
9
scripts/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build scripts
|
||||
|
||||
package main
|
||||
|
||||
import "allaboutapps.dev/aw/go-starter/scripts/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
89
scripts/sql/default_zero_values.sql
Normal file
89
scripts/sql/default_zero_values.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- Errors if DEFAULT values for certain column data_types
|
||||
-- is NOT set to the golang zero value
|
||||
--
|
||||
-- https://github.com/volatiletech/sqlboiler/issues/409
|
||||
-- https://github.com/volatiletech/sqlboiler/issues/237
|
||||
-- https://golang.org/ref/spec#The_zero_value
|
||||
--
|
||||
-- https://github.com/volatiletech/sqlboiler#diagnosing-problems
|
||||
-- A field not being inserted (usually a default true boolean), boil.Infer
|
||||
-- looks at the zero value of your Go type (it doesn’t care what the default
|
||||
-- value in the database is) to determine if it should insert your field or
|
||||
-- not. In the case of a default true boolean value, when you want to set it to
|
||||
-- false; you set that in the struct but that’s the zero value for the bool
|
||||
-- field in Go so sqlboiler assumes you do not want to insert that field and
|
||||
-- you want the default value from the database. Use a whitelist/greylist to
|
||||
-- add that field to the list of fields to insert.
|
||||
--
|
||||
-- boil.Infer() assumes all SQL defaults are Go zero value
|
||||
--
|
||||
-- To mitigate the above situation we disallow setting DEFAULT to anything
|
||||
-- other than the default golang zero value of this type. Otherwise this issue
|
||||
-- is fairly hard to debug (boil.Infer() does not insert DEFAULT as expected).
|
||||
--
|
||||
-- If a default value is actually set, we only allow the respective mapped golang zero value:
|
||||
-- 0 for all integer types
|
||||
-- 0.0 for floating point numbers
|
||||
-- false for booleans
|
||||
-- "" for strings
|
||||
-- nil for pointers
|
||||
--
|
||||
-- https://stackoverflow.com/questions/8146448/get-the-default-values-of-table-columns-in-postgres
|
||||
-- https://dba.stackexchange.com/questions/205471/why-does-information-schema-have-yes-and-no-character-strings-rather-than-bo
|
||||
CREATE OR REPLACE FUNCTION check_default_go_sql_zero_values ()
|
||||
RETURNS SETOF information_schema.columns
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE (table_schema = 'public'
|
||||
AND column_default IS NOT NULL)
|
||||
AND is_nullable = 'NO'
|
||||
AND ((data_type = 'boolean'
|
||||
AND column_default <> 'false')
|
||||
OR (data_type IN ('char', 'character', 'varchar', 'character varying', 'text')
|
||||
AND column_default NOT LIKE concat('''''', '::%'))
|
||||
OR (data_type IN ('smallint', 'integer', 'bigint', 'smallserial', 'serial', 'bigserial')
|
||||
AND (column_default <> '0'
|
||||
AND column_default NOT LIKE 'nextval(%'))
|
||||
OR (data_type IN ('decimal', 'numeric', 'real', 'double precision')
|
||||
AND column_default <> '0.0'));
|
||||
END
|
||||
$BODY$
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION default_zero_values ()
|
||||
RETURNS void
|
||||
AS $$
|
||||
DECLARE
|
||||
item record;
|
||||
BEGIN
|
||||
FOR item IN
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
column_default,
|
||||
is_nullable
|
||||
FROM
|
||||
check_default_go_sql_zero_values ()
|
||||
LOOP
|
||||
RAISE WARNING ' %.% % : INVALID DEFAULT ''%''', item.table_name, item.column_name, item.data_type, item.column_default USING HINT = to_json(item);
|
||||
END LOOP;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION 'NOT NULL columns require the respective go zero value () AS their DEFAULT value or no DEFAULT at all'
|
||||
USING HINT = '0 for integer types, 0.0 for floating point numbers, false for booleans, "" for strings';
|
||||
END IF;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql;
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
default_zero_values ();
|
||||
|
||||
57
scripts/sql/fk_missing_index.sql
Normal file
57
scripts/sql/fk_missing_index.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Errors if FKs do not have an index
|
||||
CREATE OR REPLACE FUNCTION fk_missing_index ()
|
||||
RETURNS void
|
||||
AS $$
|
||||
DECLARE
|
||||
item record;
|
||||
BEGIN
|
||||
FOR item IN
|
||||
SELECT
|
||||
c.conrelid::regclass AS "table",
|
||||
/* list of key column names in order */
|
||||
string_agg(a.attname, ',' ORDER BY x.n) AS columns,
|
||||
pg_catalog.pg_size_pretty(pg_catalog.pg_relation_size(c.conrelid)) AS size,
|
||||
c.conname AS constraint,
|
||||
c.confrelid::regclass AS referenced_table
|
||||
FROM
|
||||
pg_catalog.pg_constraint c
|
||||
/* enumerated key column numbers per foreign key */
|
||||
CROSS JOIN LATERAL unnest(c.conkey)
|
||||
WITH ORDINALITY AS x (attnum, n)
|
||||
/* name for each key column */
|
||||
JOIN pg_catalog.pg_attribute a ON a.attnum = x.attnum
|
||||
AND a.attrelid = c.conrelid
|
||||
WHERE
|
||||
NOT EXISTS
|
||||
/* is there a matching index for the constraint? */
|
||||
(
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
pg_catalog.pg_index i
|
||||
WHERE
|
||||
i.indrelid = c.conrelid
|
||||
/* the first index columns must be the same as the
|
||||
key columns, but order doesn't matter */
|
||||
AND (i.indkey::smallint[])[0:cardinality(c.conkey) - 1] @> c.conkey)
|
||||
AND c.contype = 'f'
|
||||
GROUP BY
|
||||
c.conrelid,
|
||||
c.conname,
|
||||
c.confrelid
|
||||
ORDER BY
|
||||
pg_catalog.pg_relation_size(c.conrelid) DESC LOOP
|
||||
RAISE WARNING 'CREATE INDEX "idx_%_fk_%" ON "%" ("%");', item.table, item.columns, item.table, item.columns USING HINT = to_json(item);
|
||||
END LOOP;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION ' We require ALL FOREIGN keys TO have an INDEX defined. ';
|
||||
END IF;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql;
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
fk_missing_index ();
|
||||
|
||||
15
scripts/sql/info.sql
Normal file
15
scripts/sql/info.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- This file just queries for the currently applied tables/columns as an overview
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
column_default,
|
||||
is_nullable
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE (table_schema = 'public')
|
||||
ORDER BY
|
||||
table_name,
|
||||
is_nullable,
|
||||
column_name;
|
||||
|
||||
Reference in New Issue
Block a user