// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. // // This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. package gendao import ( "context" "fmt" "sort" "strings" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "golang.org/x/mod/modfile" "git.magicany.cc/black1552/gin-base/database" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gproc" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gview" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" "git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog" "git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils" ) type ( CGenDao struct{} CGenDaoInput struct { g.Meta `name:"dao" config:"{CGenDaoConfig}" usage:"{CGenDaoUsage}" brief:"{CGenDaoBrief}" eg:"{CGenDaoEg}" ad:"{CGenDaoAd}"` Path string `name:"path" short:"p" brief:"{CGenDaoBriefPath}" d:"internal"` Link string `name:"link" short:"l" brief:"{CGenDaoBriefLink}"` Tables string `name:"tables" short:"t" brief:"{CGenDaoBriefTables}"` TablesEx string `name:"tablesEx" short:"x" brief:"{CGenDaoBriefTablesEx}"` ShardingPattern []string `name:"shardingPattern" short:"sp" brief:"{CGenDaoBriefShardingPattern}"` Group string `name:"group" short:"g" brief:"{CGenDaoBriefGroup}" d:"default"` Prefix string `name:"prefix" short:"f" brief:"{CGenDaoBriefPrefix}"` RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenDaoBriefRemovePrefix}"` RemoveFieldPrefix string `name:"removeFieldPrefix" short:"rf" brief:"{CGenDaoBriefRemoveFieldPrefix}"` JsonCase string `name:"jsonCase" short:"j" brief:"{CGenDaoBriefJsonCase}" d:"CamelLower"` ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenDaoBriefImportPrefix}"` DaoPath string `name:"daoPath" short:"d" brief:"{CGenDaoBriefDaoPath}" d:"dao"` TablePath string `name:"tablePath" short:"tp" brief:"{CGenDaoBriefTablePath}" d:"table"` DoPath string `name:"doPath" short:"o" brief:"{CGenDaoBriefDoPath}" d:"model/do"` EntityPath string `name:"entityPath" short:"e" brief:"{CGenDaoBriefEntityPath}" d:"model/entity"` TplDaoTablePath string `name:"tplDaoTablePath" short:"t0" brief:"{CGenDaoBriefTplDaoTablePath}"` TplDaoIndexPath string `name:"tplDaoIndexPath" short:"t1" brief:"{CGenDaoBriefTplDaoIndexPath}"` TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenDaoBriefTplDaoInternalPath}"` TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenDaoBriefTplDaoDoPathPath}"` TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenDaoBriefTplDaoEntityPath}"` StdTime bool `name:"stdTime" short:"s" brief:"{CGenDaoBriefStdTime}" orphan:"true"` WithTime bool `name:"withTime" short:"w" brief:"{CGenDaoBriefWithTime}" orphan:"true"` GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenDaoBriefGJsonSupport}" orphan:"true"` OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenDaoBriefOverwriteDao}" orphan:"true"` DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenDaoBriefDescriptionTag}" orphan:"true"` NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenDaoBriefNoJsonTag}" orphan:"true"` NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenDaoBriefNoModelComment}" orphan:"true"` Clear bool `name:"clear" short:"a" brief:"{CGenDaoBriefClear}" orphan:"true"` GenTable bool `name:"genTable" short:"gt" brief:"{CGenDaoBriefGenTable}" orphan:"true"` TypeMapping map[DBFieldTypeName]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenDaoBriefTypeMapping}" orphan:"true"` FieldMapping map[DBTableFieldName]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenDaoBriefFieldMapping}" orphan:"true"` // internal usage purpose. genItems *CGenDaoInternalGenItems } CGenDaoOutput struct{} CGenDaoInternalInput struct { CGenDaoInput DB database.DB TableNames []string NewTableNames []string ShardingTableSet *gset.StrSet } DBTableFieldName = string DBFieldTypeName = string CustomAttributeType struct { Type string `brief:"custom attribute type name"` Import string `brief:"custom import for this type"` } ) var ( createdAt = gtime.Now() tplView = gview.New() defaultTypeMapping = map[DBFieldTypeName]CustomAttributeType{ "decimal": { Type: "float64", }, "money": { Type: "float64", }, "numeric": { Type: "float64", }, "smallmoney": { Type: "float64", }, "uuid": { Type: "uuid.UUID", Import: "github.com/google/uuid", }, } // tablewriter Options twRenderer = tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.Border{Top: tw.Off, Bottom: tw.Off, Left: tw.Off, Right: tw.Off}, Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.Off, BetweenColumns: tw.Off}, }, Symbols: tw.NewSymbols(tw.StyleASCII), })) twConfig = tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{AutoWrap: tw.WrapNone}, }, }) ) func (c CGenDao) Dao(ctx context.Context, in CGenDaoInput) (out *CGenDaoOutput, err error) { in.genItems = newCGenDaoInternalGenItems() if in.Link != "" { doGenDaoForArray(ctx, -1, in) } else if g.Cfg().Available(ctx) { v := g.Cfg().MustGet(ctx, CGenDaoConfig) if v.IsSlice() { for i := 0; i < len(v.Interfaces()); i++ { doGenDaoForArray(ctx, i, in) } } else { doGenDaoForArray(ctx, -1, in) } } else { doGenDaoForArray(ctx, -1, in) } doClear(in.genItems) mlog.Print("done!") return } // doGenDaoForArray implements the "gen dao" command for configuration array. func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { var ( err error db database.DB ) if index >= 0 { err = g.Cfg().MustGet( ctx, fmt.Sprintf(`%s.%d`, CGenDaoConfig, index), ).Scan(&in) if err != nil { mlog.Fatalf(`invalid configuration of "%s": %+v`, CGenDaoConfig, err) } } if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" { mlog.Fatalf(`path "%s" does not exist`, in.Path) } removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",") // It uses user passed database configuration. if in.Link != "" { var tempGroup = gtime.TimestampNanoStr() err = database.AddConfigNode(tempGroup, database.ConfigNode{ Link: in.Link, }) if err != nil { mlog.Fatalf(`database configuration failed: %+v`, err) } if db, err = database.Instance(tempGroup); err != nil { mlog.Fatalf(`database initialization failed: %+v`, err) } } else { db = database.Database(in.Group) } if db == nil { mlog.Fatal(`database initialization failed, may be invalid database configuration`) } var tableNames []string if in.Tables != "" { inputTables := gstr.SplitAndTrim(in.Tables, ",") // Check if any table pattern contains wildcard characters. // https://github.com/gogf/gf/issues/4629 var hasPattern bool for _, t := range inputTables { if containsWildcard(t) { hasPattern = true break } } if hasPattern { // Fetch all tables first, then filter by patterns. allTables, err := db.Tables(context.TODO()) if err != nil { mlog.Fatalf("fetching tables failed: %+v", err) } tableNames = filterTablesByPatterns(allTables, inputTables) } else { // Use exact table names as before. tableNames = inputTables } } else { tableNames, err = db.Tables(context.TODO()) if err != nil { mlog.Fatalf("fetching tables failed: %+v", err) } } // Table excluding. if in.TablesEx != "" { array := garray.NewStrArrayFrom(tableNames) for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") { if containsWildcard(p) { // Use exact match with ^ and $ anchors for consistency with tables pattern. regPattern := "^" + patternToRegex(p) + "$" for _, v := range array.Clone().Slice() { if gregex.IsMatchString(regPattern, v) { array.RemoveValue(v) } } } else { array.RemoveValue(p) } } tableNames = array.Slice() } // merge default typeMapping to input typeMapping. if in.TypeMapping == nil { in.TypeMapping = defaultTypeMapping } else { for key, typeMapping := range defaultTypeMapping { if _, ok := in.TypeMapping[key]; !ok { in.TypeMapping[key] = typeMapping } } } // Generating dao & model go files one by one according to given table name. var ( newTableNames = make([]string, len(tableNames)) shardingNewTableSet = gset.NewStrSet() ) // Sort sharding patterns by length descending, so that longer (more specific) patterns // are matched first. This prevents shorter patterns like "a_?" from incorrectly matching // tables that should match longer patterns like "a_b_?" or "a_c_?". // https://github.com/gogf/gf/issues/4603 sortedShardingPatterns := make([]string, len(in.ShardingPattern)) copy(sortedShardingPatterns, in.ShardingPattern) sort.Slice(sortedShardingPatterns, func(i, j int) bool { return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j]) }) for i, tableName := range tableNames { newTableName := tableName for _, v := range removePrefixArray { newTableName = gstr.TrimLeftStr(newTableName, v, 1) } if len(sortedShardingPatterns) > 0 { for _, pattern := range sortedShardingPatterns { var ( match []string regPattern = gstr.Replace(pattern, "?", `(.+)`) ) match, err = gregex.MatchString(regPattern, newTableName) if err != nil { mlog.Fatalf(`invalid sharding pattern "%s": %+v`, pattern, err) } if len(match) < 2 { continue } newTableName = gstr.Replace(pattern, "?", "") newTableName = gstr.Trim(newTableName, `_.-`) if shardingNewTableSet.Contains(newTableName) { tableNames[i] = "" break } // Add prefix to sharding table name, if not, the isSharding check would not match. shardingNewTableSet.Add(in.Prefix + newTableName) break } } newTableName = in.Prefix + newTableName if tableNames[i] != "" { // If shardingNewTableSet contains newTableName (tableName is empty), it should not be added to tableNames, make it empty and filter later. newTableNames[i] = newTableName } } tableNames = garray.NewStrArrayFrom(tableNames).FilterEmpty().Slice() newTableNames = garray.NewStrArrayFrom(newTableNames).FilterEmpty().Slice() // Filter empty table names. make sure that newTableNames and tableNames have the same length. in.genItems.Scale() // Dao: index and internal. generateDao(ctx, CGenDaoInternalInput{ CGenDaoInput: in, DB: db, TableNames: tableNames, NewTableNames: newTableNames, ShardingTableSet: shardingNewTableSet, }) // Table: table fields. generateTable(ctx, CGenDaoInternalInput{ CGenDaoInput: in, DB: db, TableNames: tableNames, NewTableNames: newTableNames, ShardingTableSet: shardingNewTableSet, }) // Do. generateDo(ctx, CGenDaoInternalInput{ CGenDaoInput: in, DB: db, TableNames: tableNames, NewTableNames: newTableNames, }) // Entity. generateEntity(ctx, CGenDaoInternalInput{ CGenDaoInput: in, DB: db, TableNames: tableNames, NewTableNames: newTableNames, }) in.genItems.SetClear(in.Clear) } func getImportPartContent(ctx context.Context, source string, isDo bool, appendImports []string) string { var packageImportsArray = garray.NewStrArray() if isDo { packageImportsArray.Append(`"github.com/gogf/gf/v2/frame/g"`) } // Time package recognition. if strings.Contains(source, "gtime.Time") { packageImportsArray.Append(`"github.com/gogf/gf/v2/os/gtime"`) } else if strings.Contains(source, "time.Time") { packageImportsArray.Append(`"time"`) } // Json type. if strings.Contains(source, "gjson.Json") { packageImportsArray.Append(`"github.com/gogf/gf/v2/encoding/gjson"`) } // Check and update imports in go.mod if len(appendImports) > 0 { goModPath := utils.GetModPath() if goModPath == "" { mlog.Fatal("go.mod not found in current project") } mod, err := modfile.Parse(goModPath, gfile.GetBytes(goModPath), nil) if err != nil { mlog.Fatalf("parse go.mod failed: %+v", err) } for _, appendImport := range appendImports { found := false for _, require := range mod.Require { if gstr.Contains(appendImport, require.Mod.Path) { found = true break } } if !found { if err = gproc.ShellRun(ctx, `go get `+appendImport); err != nil { mlog.Fatalf(`%+v`, err) } } packageImportsArray.Append(fmt.Sprintf(`"%s"`, appendImport)) } } // Generate and write content to golang file. packageImportsStr := "" if packageImportsArray.Len() > 0 { packageImportsStr = fmt.Sprintf("import(\n%s\n)", packageImportsArray.Join("\n")) } return packageImportsStr } func assignDefaultVar(view *gview.View, in CGenDaoInternalInput) { var ( tplCreatedAtDatetimeStr string tplDatetimeStr = createdAt.String() ) if in.WithTime { tplCreatedAtDatetimeStr = fmt.Sprintf(`Created at %s`, tplDatetimeStr) } view.Assigns(g.Map{ tplVarDatetimeStr: tplDatetimeStr, tplVarCreatedAtDatetimeStr: tplCreatedAtDatetimeStr, }) } func sortFieldKeyForDao(fieldMap map[string]*database.TableField) []string { names := make(map[int]string) for _, field := range fieldMap { names[field.Index] = field.Name } var ( i = 0 j = 0 result = make([]string, len(names)) ) for { if len(names) == 0 { break } if val, ok := names[i]; ok { result[j] = val j++ delete(names, i) } i++ } return result } func getTemplateFromPathOrDefault(filePath string, def string) string { if filePath != "" { if contents := gfile.GetContents(filePath); contents != "" { return contents } } return def } // containsWildcard checks if the pattern contains wildcard characters (* or ?). func containsWildcard(pattern string) bool { return gstr.Contains(pattern, "*") || gstr.Contains(pattern, "?") } // patternToRegex converts a wildcard pattern to a regex pattern. // Wildcard characters: * matches any characters, ? matches single character. func patternToRegex(pattern string) string { pattern = gstr.ReplaceByMap(pattern, map[string]string{ "\r": "", "\n": "", }) pattern = gstr.ReplaceByMap(pattern, map[string]string{ "*": "\r", "?": "\n", }) pattern = gregex.Quote(pattern) pattern = gstr.ReplaceByMap(pattern, map[string]string{ "\r": ".*", "\n": ".", }) return pattern } // filterTablesByPatterns filters tables by given patterns. // Patterns support wildcard characters: * matches any characters, ? matches single character. // https://github.com/gogf/gf/issues/4629 func filterTablesByPatterns(allTables []string, patterns []string) []string { var result []string matched := make(map[string]bool) allTablesSet := make(map[string]bool) for _, t := range allTables { allTablesSet[t] = true } for _, p := range patterns { if containsWildcard(p) { regPattern := "^" + patternToRegex(p) + "$" for _, table := range allTables { if !matched[table] && gregex.IsMatchString(regPattern, table) { result = append(result, table) matched[table] = true } } } else { // Exact table name, use direct string comparison. if !allTablesSet[p] { mlog.Printf(`table "%s" does not exist, skipped`, p) continue } if !matched[p] { result = append(result, p) matched[p] = true } } } return result }