(Feat): Initial Commit
Some checks failed
Build & Test / build-test (push) Has been cancelled
Build & Test / swagger-codegen-cli (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled

This commit is contained in:
2026-07-03 19:41:31 +05:30
commit 7e940c83a7
461 changed files with 45002 additions and 0 deletions

42
scripts/README.md Normal file
View 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
View 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)
}

View 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.")
}

View 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
View 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
View 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
View 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)
}
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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, &params)
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, &params)
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, &params)
if err != nil {
return err
}
id := params.ID.String()
// TODO: Implement
*/
return c.NoContent(http.StatusNoContent)
}
}
`
)

View 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
}

View 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
}

View 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
View File

@@ -0,0 +1,9 @@
//go:build scripts
package main
import "allaboutapps.dev/aw/go-starter/scripts/cmd"
func main() {
cmd.Execute()
}

View 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 doesnt 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 thats 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 ();

View 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
View 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;