From 829374cc1435e33ff031c261e718adf82181bdd1 Mon Sep 17 00:00:00 2001 From: sk <123456@qq.com> Date: Thu, 5 Dec 2024 13:33:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4mongoctl=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/mongox/export.go | 100 ---- core/mongox/function.go | 156 ++++++ core/tools/mongoctl/.gitignore | 3 + core/tools/mongoctl/README-zh.md | 483 ++++++++++++++++++ core/tools/mongoctl/counter.go | 51 ++ core/tools/mongoctl/example/dao/counter.go | 14 + .../mongoctl/example/dao/internal/counter.go | 76 +++ .../mongoctl/example/dao/internal/mail.go | 290 +++++++++++ .../mongoctl/example/dao/internal/user.go | 330 ++++++++++++ core/tools/mongoctl/example/dao/mail.go | 22 + core/tools/mongoctl/example/dao/user.go | 22 + core/tools/mongoctl/example/main.go | 62 +++ core/tools/mongoctl/example/model/mail.go | 16 + core/tools/mongoctl/example/model/user.go | 61 +++ core/tools/mongoctl/generator.go | 395 ++++++++++++++ core/tools/mongoctl/go.mod | 22 + core/tools/mongoctl/go.sum | 54 ++ core/tools/mongoctl/main.go | 66 +++ core/tools/mongoctl/model.go | 273 ++++++++++ core/tools/mongoctl/template/counter.go | 97 ++++ core/tools/mongoctl/template/template.go | 292 +++++++++++ core/tools/mongoctl/util.go | 160 ++++++ 22 files changed, 2945 insertions(+), 100 deletions(-) create mode 100644 core/mongox/function.go create mode 100644 core/tools/mongoctl/.gitignore create mode 100644 core/tools/mongoctl/README-zh.md create mode 100644 core/tools/mongoctl/counter.go create mode 100644 core/tools/mongoctl/example/dao/counter.go create mode 100644 core/tools/mongoctl/example/dao/internal/counter.go create mode 100644 core/tools/mongoctl/example/dao/internal/mail.go create mode 100644 core/tools/mongoctl/example/dao/internal/user.go create mode 100644 core/tools/mongoctl/example/dao/mail.go create mode 100644 core/tools/mongoctl/example/dao/user.go create mode 100644 core/tools/mongoctl/example/main.go create mode 100644 core/tools/mongoctl/example/model/mail.go create mode 100644 core/tools/mongoctl/example/model/user.go create mode 100644 core/tools/mongoctl/generator.go create mode 100644 core/tools/mongoctl/go.mod create mode 100644 core/tools/mongoctl/go.sum create mode 100644 core/tools/mongoctl/main.go create mode 100644 core/tools/mongoctl/model.go create mode 100644 core/tools/mongoctl/template/counter.go create mode 100644 core/tools/mongoctl/template/template.go create mode 100644 core/tools/mongoctl/util.go diff --git a/core/mongox/export.go b/core/mongox/export.go index b23fe44..6119ef2 100644 --- a/core/mongox/export.go +++ b/core/mongox/export.go @@ -3,20 +3,11 @@ package mongox import ( "errors" - "go.mongodb.org/mongo-driver/mongo" "mongo.games.com/goserver/core/logger" "mongo.games.com/goserver/core/mongox/internal" ) -type DatabaseType string - -const ( - KeyGlobal = "global" - DatabaseUser DatabaseType = "user" - DatabaseLog DatabaseType = "log" -) - var NotInitError = errors.New("mongo manager is nil, please call Init() first") type Config = internal.Config @@ -52,94 +43,3 @@ func Restart() { func Close() { internal.Close(_manager) } - -// GetDatabase 获取数据库 -// platform: 平台id -// database: 数据库名称 -func GetDatabase(platform string, database DatabaseType) (*Database, error) { - if _manager == nil { - return nil, NotInitError - } - - return _manager.GetDatabase(platform, string(database)) -} - -func GetUserDatabase(platform string) (*Database, error) { - return GetDatabase(platform, DatabaseUser) -} - -func GetLogDatabase(platform string) (*Database, error) { - return GetDatabase(platform, DatabaseLog) -} - -// GetGlobalDatabase 获取全局库 -// database: 数据库名称 -func GetGlobalDatabase(database DatabaseType) (*Database, error) { - if _manager == nil { - return nil, NotInitError - } - - return _manager.GetDatabase(KeyGlobal, string(database)) -} - -func GetGlobalUserDatabase() (*Database, error) { - return GetGlobalDatabase(DatabaseUser) -} - -func GetGlobalLogDatabase() (*Database, error) { - return GetGlobalDatabase(DatabaseLog) -} - -// GetGlobalCollection 获取全局库 -// database: 数据库名称 -// collection: 集合名称 -func GetGlobalCollection(database DatabaseType, collection string) (*Collection, error) { - if _manager == nil { - return nil, NotInitError - } - - return _manager.GetCollection(KeyGlobal, string(database), collection) -} - -func GetGlobalUserCollection(collection string) (*Collection, error) { - return GetGlobalCollection(DatabaseUser, collection) -} - -func GetGlobalLogCollection(collection string) (*Collection, error) { - return GetGlobalCollection(DatabaseLog, collection) -} - -// GetCollection 获取平台库 -// platform: 平台id -// database: 数据库名称 -// collection: 集合名称 -func GetCollection(platform string, database DatabaseType, collection string) (*Collection, error) { - if _manager == nil { - return nil, NotInitError - } - - return _manager.GetCollection(platform, string(database), collection) -} - -func GetUserCollection(platform string, collection string) (*Collection, error) { - return GetCollection(platform, DatabaseUser, collection) -} - -func GetLogCollection(platform string, collection string) (*Collection, error) { - return GetCollection(platform, DatabaseLog, collection) -} - -// GetClient 获取数据库连接 -// 默认获取的是 Global, log 的数据库连接 -func GetClient() (*mongo.Client, error) { - if _manager == nil { - return nil, NotInitError - } - - c, err := _manager.GetCollection(KeyGlobal, string(DatabaseLog), "empty") - if err != nil { - return nil, err - } - - return c.Database.Client, nil -} diff --git a/core/mongox/function.go b/core/mongox/function.go new file mode 100644 index 0000000..75a40d9 --- /dev/null +++ b/core/mongox/function.go @@ -0,0 +1,156 @@ +package mongox + +import ( + "reflect" + "strings" + + "go.mongodb.org/mongo-driver/mongo" + + "mongo.games.com/goserver/core/logger" +) + +type DatabaseType string + +const ( + KeyGlobal = "global" + + DatabaseUser DatabaseType = "user" + DatabaseLog DatabaseType = "log" + DatabaseMonitor DatabaseType = "monitor" +) + +// GetClient 获取数据库连接 +// 默认获取的是 Global, log 的数据库连接 +func GetClient() (*mongo.Client, error) { + if _manager == nil { + return nil, NotInitError + } + + c, err := _manager.GetCollection(KeyGlobal, string(DatabaseLog), "empty") + if err != nil { + return nil, err + } + + return c.Database.Client, nil +} + +// GetDatabase 获取数据库 +// platform: 平台id +// database: 数据库名称 +func GetDatabase(platform string, database DatabaseType) (*Database, error) { + if _manager == nil { + return nil, NotInitError + } + + return _manager.GetDatabase(platform, string(database)) +} + +func GetUserDatabase(platform string) (*Database, error) { + return GetDatabase(platform, DatabaseUser) +} + +func GetLogDatabase(platform string) (*Database, error) { + return GetDatabase(platform, DatabaseLog) +} + +// GetGlobalDatabase 获取全局库 +// database: 数据库名称 +func GetGlobalDatabase(database DatabaseType) (*Database, error) { + if _manager == nil { + return nil, NotInitError + } + + return _manager.GetDatabase(KeyGlobal, string(database)) +} + +func GetGlobalUserDatabase() (*Database, error) { + return GetGlobalDatabase(DatabaseUser) +} + +func GetGlobalLogDatabase() (*Database, error) { + return GetGlobalDatabase(DatabaseLog) +} + +func GetGlobalMonitorDatabase() (*Database, error) { + return GetGlobalDatabase(DatabaseMonitor) +} + +// GetGlobalCollection 获取全局库 +// database: 数据库名称 +// collection: 集合名称 +func GetGlobalCollection(database DatabaseType, collection string) (*Collection, error) { + if _manager == nil { + return nil, NotInitError + } + + return _manager.GetCollection(KeyGlobal, string(database), collection) +} + +func GetGlobalUserCollection(collection string) (*Collection, error) { + return GetGlobalCollection(DatabaseUser, collection) +} + +func GetGlobalLogCollection(collection string) (*Collection, error) { + return GetGlobalCollection(DatabaseLog, collection) +} + +func GetGlobalMonitorCollection(collection string) (*Collection, error) { + return GetGlobalCollection(DatabaseMonitor, collection) +} + +// GetCollection 获取平台库 +// platform: 平台id +// database: 数据库名称 +// collection: 集合名称 +func GetCollection(platform string, database DatabaseType, collection string) (*Collection, error) { + if _manager == nil { + return nil, NotInitError + } + + return _manager.GetCollection(platform, string(database), collection) +} + +func GetUserCollection(platform string, collection string) (*Collection, error) { + return GetCollection(platform, DatabaseUser, collection) +} + +func GetLogCollection(platform string, collection string) (*Collection, error) { + return GetCollection(platform, DatabaseLog, collection) +} + +// ICollectionName 文档名称接口 +type ICollectionName interface { + CollectionName() string +} + +// GetTableName 获取文档名 +func GetTableName(model any) string { + if m, ok := model.(ICollectionName); ok { + return m.CollectionName() + } + + t := reflect.TypeOf(model) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + panic("model must be a struct or a pointer to a struct") + } + + return strings.ToLower(t.Name()) +} + +// GetCollectionDao 获取文档操作接口 +// key: 平台id 或 KeyGlobal +// database: 数据库类型 DatabaseType +// f: 文档接口创建函数 +func GetCollectionDao[T any](key string, database DatabaseType, model any, f func(database *mongo.Database, c *mongo.Collection) T) (T, error) { + collectionName := GetTableName(model) + c, err := GetCollection(key, database, collectionName) + if err != nil { + var z T + logger.Logger.Errorf("GetCollectionModel key:%v database:%v model:%v error: %v", key, database, collectionName, err) + return z, err + } + return f(c.Database.Database, c.Collection), nil +} diff --git a/core/tools/mongoctl/.gitignore b/core/tools/mongoctl/.gitignore new file mode 100644 index 0000000..d5bbd2b --- /dev/null +++ b/core/tools/mongoctl/.gitignore @@ -0,0 +1,3 @@ +.idea/ +*/.DS_Store +.vscode \ No newline at end of file diff --git a/core/tools/mongoctl/README-zh.md b/core/tools/mongoctl/README-zh.md new file mode 100644 index 0000000..a498910 --- /dev/null +++ b/core/tools/mongoctl/README-zh.md @@ -0,0 +1,483 @@ +# mongoctl + +### 1.介绍 + +mongoctl是一个自动化生成MongoDB数据访问对象(Data Access Object)的工具。 + +### 2.优势 + +* 支持primitive.ObjectID、primitive.DateTime类型的自动填充。 + +* 支持int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64类型的自增长。 + +* 提供了对数据库字段的统一生成方案,避免了业务代码中随处可见的数据库字段的问题。 + +* 提供了包括InsertOne、InsertMany、UpdateOne、UpdateOneByID、UpdateMany、FindOne、FindOneByID、FindMany、DeleteOne、DeleteOneByID、DeleteMany、Count、Aggregate等多种数据库操作接口。 + +* 提供了对数据库操作接口的扩展能力。 + +* 提供了分包与不分包两种包解决方案。 + +* 支持目录和文件名风格的自定义。 + +### 3.安装 + +```bash +go install +``` + +### 4.用法 + +```bash +用法 mongoctl: + mongoctl [flags] -model-dir=. -model-names=T,T -dao-dir=./dao +Flags: + -counter-name string + 自增模型名称; 默认是 counter + -dao-dir string + 生成代码所在目录; 必需 + -dao-pkg-path string + 生成代码所在目录的包名; 默认自动生成 + -file-style string + 代码文件名称风格; 选项: kebab | underscore | lower | camel | pascal; 默认是 underscore (default "underscore") + -model-dir string + 模型所在目录; 必需 + -model-names string + 模型结构体名称; 必需 + -model-pkg-alias string + 模型包别名 + -model-pkg-path string + 模型包名; 默认自动生成 + -sub-pkg-enable + 每个模型创建一个子包; 默认关闭 + -sub-pkg-style string + 模型子包目录名称风格; 选项: kebab | underscore | lower | camel | pascal; 默认是 kebab (default "kebab") + +``` + +### 5.标签 + +在模型定义中支持对gen标签的解析,目前支持以下标签解析: + +| 标签名称 | | 示例 | 说明 | +| -------- | ---------------------------------------------------------- | ------------------ | -------------------------- | +| autoFill | primitive.ObjectID、primitive.DateTime | gen:"autoFill" | | +| autoIncr | int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64 | gen:"autoIncr:uid" | 该自增为原子操作,会在生成代码时同步生成计数器代码。 | + +### 6.示例 + +###### 6-1.创建模型 + +model/mail.go + +```go +package model + +import "go.mongodb.org/mongo-driver/bson/primitive" + +//go:generate mongoctl -model-dir=. -model-names=Mail -dao-dir=../dao/ +type Mail struct { + ID primitive.ObjectID `bson:"_id" gen:"autoFill"` // 邮件ID + Title string `bson:"title"` // 邮件标题 + Content string `bson:"content"` // 邮件内容 + Sender int64 `bson:"sender"` // 邮件发送者 + Receiver int64 `bson:"receiver"` // 邮件接受者 + Status int `bson:"status"` // 邮件状态 + SendTime primitive.DateTime `bson:"send_time" gen:"autoFill"` // 发送时间 +} +``` + +###### 6-2.生成dao文件 + +```bash +go generate ./... +``` + +###### 6-3.生成的dao文件示例 + +dao/internal/mail.go + +```go +// -------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------- + +package internal + +import ( + "context" + "errors" + models "github.com/dobyte/mongo-dao-generator/example/model" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type MailFilterFunc func(cols *MailColumns) interface{} +type MailUpdateFunc func(cols *MailColumns) interface{} +type MailPipelineFunc func(cols *MailColumns) interface{} +type MailCountOptionsFunc func(cols *MailColumns) *options.CountOptions +type MailAggregateOptionsFunc func(cols *MailColumns) *options.AggregateOptions +type MailFindOneOptionsFunc func(cols *MailColumns) *options.FindOneOptions +type MailFindManyOptionsFunc func(cols *MailColumns) *options.FindOptions +type MailUpdateOptionsFunc func(cols *MailColumns) *options.UpdateOptions +type MailDeleteOptionsFunc func(cols *MailColumns) *options.DeleteOptions +type MailInsertOneOptionsFunc func(cols *MailColumns) *options.InsertOneOptions +type MailInsertManyOptionsFunc func(cols *MailColumns) *options.InsertManyOptions + +type Mail struct { + Columns *MailColumns + Database *mongo.Database + Collection *mongo.Collection +} + +type MailColumns struct { + ID string // 邮件ID + Title string // 邮件标题 + Content string // 邮件内容 + Sender string // 邮件发送者 + Receiver string // 邮件接受者 + Status string // 邮件状态 + SendTime string // 发送时间 +} + +var mailColumns = &MailColumns{ + ID: "_id", // 邮件ID + Title: "title", // 邮件标题 + Content: "content", // 邮件内容 + Sender: "sender", // 邮件发送者 + Receiver: "receiver", // 邮件接受者 + Status: "status", // 邮件状态 + SendTime: "send_time", // 发送时间 +} + +func NewMail(db *mongo.Database) *Mail { + return &Mail{ + Columns: mailColumns, + Database: db, + Collection: db.Collection("mail"), + } +} + +// Count returns the number of documents in the collection. +func (dao *Mail) Count(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailCountOptionsFunc) (int64, error) { + var ( + opts *options.CountOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.CountDocuments(ctx, filter, opts) +} + +// Aggregate executes an aggregate command against the collection and returns a cursor over the resulting documents. +func (dao *Mail) Aggregate(ctx context.Context, pipelineFunc MailPipelineFunc, optionsFunc ...MailAggregateOptionsFunc) (*mongo.Cursor, error) { + var ( + opts *options.AggregateOptions + pipeline = pipelineFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.Aggregate(ctx, pipeline, opts) +} + +// InsertOne executes an insert command to insert a single document into the collection. +func (dao *Mail) InsertOne(ctx context.Context, model *models.Mail, optionsFunc ...MailInsertOneOptionsFunc) (*mongo.InsertOneResult, error) { + if model == nil { + return nil, errors.New("model is nil") + } + + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + + var opts *options.InsertOneOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertOne(ctx, model, opts) +} + +// InsertMany executes an insert command to insert multiple documents into the collection. +func (dao *Mail) InsertMany(ctx context.Context, models []*models.Mail, optionsFunc ...MailInsertManyOptionsFunc) (*mongo.InsertManyResult, error) { + if len(models) == 0 { + return nil, errors.New("models is empty") + } + + documents := make([]interface{}, 0, len(models)) + for i := range models { + model := models[i] + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + documents = append(documents, model) + } + + var opts *options.InsertManyOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertMany(ctx, documents, opts) +} + +// UpdateOne executes an update command to update at most one document in the collection. +func (dao *Mail) UpdateOne(ctx context.Context, filterFunc MailFilterFunc, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateOne(ctx, filter, update, opts) +} + +// UpdateOneByID executes an update command to update at most one document in the collection. +func (dao *Mail) UpdateOneByID(ctx context.Context, id string, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.UpdateOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, updateFunc, optionsFunc...) +} + +// UpdateMany executes an update command to update documents in the collection. +func (dao *Mail) UpdateMany(ctx context.Context, filterFunc MailFilterFunc, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateMany(ctx, filter, update, opts) +} + +// FindOne executes a find command and returns a model for one document in the collection. +func (dao *Mail) FindOne(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailFindOneOptionsFunc) (*models.Mail, error) { + var ( + opts *options.FindOneOptions + model = &models.Mail{} + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + err := dao.Collection.FindOne(ctx, filter, opts).Decode(model) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + return model, nil +} + +// FindOneByID executes a find command and returns a model for one document in the collection. +func (dao *Mail) FindOneByID(ctx context.Context, id string, optionsFunc ...MailFindOneOptionsFunc) (*models.Mail, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.FindOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// FindMany executes a find command and returns many models the matching documents in the collection. +func (dao *Mail) FindMany(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailFindManyOptionsFunc) ([]*models.Mail, error) { + var ( + opts *options.FindOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + cur, err := dao.Collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + + models := make([]*models.Mail, 0) + + if err = cur.All(ctx, &models); err != nil { + return nil, err + } + + return models, nil +} + +// DeleteOne executes a delete command to delete at most one document from the collection. +func (dao *Mail) DeleteOne(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteOne(ctx, filter, opts) +} + +// DeleteOneByID executes a delete command to delete at most one document from the collection. +func (dao *Mail) DeleteOneByID(ctx context.Context, id string, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.DeleteOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// DeleteMany executes a delete command to delete documents from the collection. +func (dao *Mail) DeleteMany(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteMany(ctx, filter, opts) +} + +// autofill when inserting data +func (dao *Mail) autofill(ctx context.Context, model *models.Mail) error { + if model.ID.IsZero() { + model.ID = primitive.NewObjectID() + } + + if model.SendTime == 0 { + model.SendTime = primitive.NewDateTimeFromTime(time.Now()) + } + + return nil +} +``` + +dao/mail.go + +```go +package dao + +import ( + "github.com/dobyte/mongo-dao-generator/example/dao/internal" + "go.mongodb.org/mongo-driver/mongo" +) + +type MailColumns = internal.MailColumns + +type Mail struct { + *internal.Mail +} + +func NewMail(db *mongo.Database) *Mail { + return &Mail{Mail: internal.NewMail(db)} +} +``` + +###### 6-4.使用生成的dao文件 + +```go +package main + +import ( + "context" + "github.com/dobyte/mongo-dao-generator/example/dao" + "github.com/dobyte/mongo-dao-generator/example/model" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" + "log" + "time" +) + +func main() { + var ( + uri = "mongodb://root:12345678@127.0.0.1:27017" + opts = options.Client().ApplyURI(uri) + baseCtx = context.Background() + ) + + ctx, cancel := context.WithTimeout(baseCtx, 5*time.Second) + client, err := mongo.Connect(ctx, opts) + cancel() + if err != nil { + log.Fatalf("connect mongo server failed: %v", err) + } + + ctx, cancel = context.WithTimeout(baseCtx, 5*time.Second) + defer cancel() + err = client.Ping(ctx, readpref.Primary()) + cancel() + if err != nil { + log.Fatalf("ping mongo server failed: %v", err) + } + + db := client.Database("dao_test") + + mailDao := dao.NewMail(db) + + _, err = mailDao.InsertOne(baseCtx, &model.Mail{ + Title: "mongo-dao-generator introduction", + Content: "the mongo-dao-generator is a tool for automatically generating MongoDB Data Access Object.", + Sender: 1, + Receiver: 2, + Status: 1, + }) + if err != nil { + log.Fatalf("failed to insert into mongo database: %v", err) + } + + mail, err := mailDao.FindOne(baseCtx, func(cols *dao.MailColumns) interface{} { + return bson.M{cols.Receiver: 2} + }) + if err != nil { + log.Fatalf("failed to find a row of data from mongo database: %v", err) + } + + log.Printf("%+v", mail) +} +``` + +运行结果: + +```bash +2023/02/17 16:05:31 &{ID:ObjectID("63ef354a4ddc485f0d9c5ea3") Title:mongo-dao-generator introduction Content:the mongo-dao-generator is a tool for automatically generating MongoDB Data Access Object. Sender:1 Receiver:2 Status:1 SendTime:1676621130323} +``` diff --git a/core/tools/mongoctl/counter.go b/core/tools/mongoctl/counter.go new file mode 100644 index 0000000..e128ee6 --- /dev/null +++ b/core/tools/mongoctl/counter.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "path/filepath" + "strings" +) + +type counter struct { + opts *options + modelName string + daoClassName string + daoVariableName string + daoPkgPath string + daoPkgName string + daoOutputDir string + daoOutputFile string + daoPrefixName string + collectionName string +} + +func newCounter(opts *options) *counter { + c := &counter{} + c.opts = opts + c.modelName = toPascalCase(opts.counterName) + c.daoClassName = toPascalCase(c.modelName) + c.daoVariableName = toCamelCase(c.modelName) + c.daoOutputFile = fmt.Sprintf("%s.go", toFileName(c.modelName, c.opts.fileNameStyle)) + c.collectionName = toUnderscoreCase(c.modelName) + + dir := strings.TrimSuffix(opts.daoDir, "/") + + if opts.subPkgEnable { + c.daoOutputDir = dir + "/" + toPackagePath(c.modelName, c.opts.subPkgStyle) + } else { + c.daoOutputDir = dir + c.daoPrefixName = toPascalCase(c.modelName) + } + + return c +} + +func (c *counter) setDaoPkgPath(path string) { + if c.opts.subPkgEnable { + c.daoPkgPath = path + "/" + toPackagePath(c.modelName, c.opts.subPkgStyle) + } else { + c.daoPkgPath = path + } + + c.daoPkgName = toPackageName(filepath.Base(c.daoPkgPath)) +} diff --git a/core/tools/mongoctl/example/dao/counter.go b/core/tools/mongoctl/example/dao/counter.go new file mode 100644 index 0000000..9b28397 --- /dev/null +++ b/core/tools/mongoctl/example/dao/counter.go @@ -0,0 +1,14 @@ +package dao + +import ( + "go.mongodb.org/mongo-driver/mongo" + "mongo.games.com/goserver/mongoctl/example/dao/internal" +) + +type Counter struct { + *internal.Counter +} + +func NewCounter(db *mongo.Database) *Counter { + return &Counter{Counter: internal.NewCounter(db)} +} diff --git a/core/tools/mongoctl/example/dao/internal/counter.go b/core/tools/mongoctl/example/dao/internal/counter.go new file mode 100644 index 0000000..e220cd4 --- /dev/null +++ b/core/tools/mongoctl/example/dao/internal/counter.go @@ -0,0 +1,76 @@ +// -------------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------------- + +package internal + +import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Counter struct { + Columns *CounterColumns + Database *mongo.Database + Collection *mongo.Collection +} + +type CounterModel struct { + ID string `bson:"_id"` + Value int64 `bson:"value"` +} + +type CounterColumns struct { + ID string + Value string +} + +var counterColumns = &CounterColumns{ + ID: "_id", + Value: "value", +} + +func NewCounter(db *mongo.Database) *Counter { + return &Counter{ + Columns: counterColumns, + Database: db, + Collection: db.Collection("counter"), + } +} + +// Incr 自增值 +func (dao *Counter) Incr(ctx context.Context, key string, incr ...int) (int64, error) { + var ( + upsert = true + returnDocument = options.After + counter = &CounterModel{} + value = 1 + ) + + if len(incr) > 0 { + if incr[0] == 0 { + return 0, errors.New("invalid increment value") + } + value = incr[0] + } + + rst := dao.Collection.FindOneAndUpdate(ctx, bson.M{ + dao.Columns.ID: key, + }, bson.M{"$inc": bson.M{ + dao.Columns.Value: value, + }}, &options.FindOneAndUpdateOptions{ + Upsert: &upsert, + ReturnDocument: &returnDocument, + }) + + if err := rst.Decode(counter); err != nil { + return 0, err + } + + return counter.Value, nil +} diff --git a/core/tools/mongoctl/example/dao/internal/mail.go b/core/tools/mongoctl/example/dao/internal/mail.go new file mode 100644 index 0000000..3e5557f --- /dev/null +++ b/core/tools/mongoctl/example/dao/internal/mail.go @@ -0,0 +1,290 @@ +// -------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------- + +package internal + +import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + modelpkg "mongo.games.com/goserver/mongoctl/example/model" + "time" +) + +type MailFilterFunc func(cols *MailColumns) interface{} +type MailUpdateFunc func(cols *MailColumns) interface{} +type MailPipelineFunc func(cols *MailColumns) interface{} +type MailCountOptionsFunc func(cols *MailColumns) *options.CountOptions +type MailAggregateOptionsFunc func(cols *MailColumns) *options.AggregateOptions +type MailFindOneOptionsFunc func(cols *MailColumns) *options.FindOneOptions +type MailFindManyOptionsFunc func(cols *MailColumns) *options.FindOptions +type MailUpdateOptionsFunc func(cols *MailColumns) *options.UpdateOptions +type MailDeleteOptionsFunc func(cols *MailColumns) *options.DeleteOptions +type MailInsertOneOptionsFunc func(cols *MailColumns) *options.InsertOneOptions +type MailInsertManyOptionsFunc func(cols *MailColumns) *options.InsertManyOptions + +type Mail struct { + Columns *MailColumns + Database *mongo.Database + Collection *mongo.Collection +} + +type MailColumns struct { + ID string // 邮件ID + Title string // 邮件标题 + Content string // 邮件内容 + Sender string // 邮件发送者 + Receiver string // 邮件接受者 + Status string // 邮件状态 + SendTime string // 发送时间 +} + +var mailColumns = &MailColumns{ + ID: "_id", // 邮件ID + Title: "title", // 邮件标题 + Content: "content", // 邮件内容 + Sender: "sender", // 邮件发送者 + Receiver: "receiver", // 邮件接受者 + Status: "status", // 邮件状态 + SendTime: "send_time", // 发送时间 +} + +func NewMail(db *mongo.Database) *Mail { + return &Mail{ + Columns: mailColumns, + Database: db, + Collection: db.Collection("mail"), + } +} + +// Count returns the number of documents in the collection. +func (dao *Mail) Count(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailCountOptionsFunc) (int64, error) { + var ( + opts *options.CountOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.CountDocuments(ctx, filter, opts) +} + +// Aggregate executes an aggregate command against the collection and returns a cursor over the resulting documents. +func (dao *Mail) Aggregate(ctx context.Context, pipelineFunc MailPipelineFunc, optionsFunc ...MailAggregateOptionsFunc) (*mongo.Cursor, error) { + var ( + opts *options.AggregateOptions + pipeline = pipelineFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.Aggregate(ctx, pipeline, opts) +} + +// InsertOne executes an insert command to insert a single document into the collection. +func (dao *Mail) InsertOne(ctx context.Context, model *modelpkg.Mail, optionsFunc ...MailInsertOneOptionsFunc) (*mongo.InsertOneResult, error) { + if model == nil { + return nil, errors.New("model is nil") + } + + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + + var opts *options.InsertOneOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertOne(ctx, model, opts) +} + +// InsertMany executes an insert command to insert multiple documents into the collection. +func (dao *Mail) InsertMany(ctx context.Context, models []*modelpkg.Mail, optionsFunc ...MailInsertManyOptionsFunc) (*mongo.InsertManyResult, error) { + if len(models) == 0 { + return nil, errors.New("models is empty") + } + + documents := make([]interface{}, 0, len(models)) + for i := range models { + model := models[i] + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + documents = append(documents, model) + } + + var opts *options.InsertManyOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertMany(ctx, documents, opts) +} + +// UpdateOne executes an update command to update at most one document in the collection. +func (dao *Mail) UpdateOne(ctx context.Context, filterFunc MailFilterFunc, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateOne(ctx, filter, update, opts) +} + +// UpdateOneByID executes an update command to update at most one document in the collection. +func (dao *Mail) UpdateOneByID(ctx context.Context, id string, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.UpdateOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, updateFunc, optionsFunc...) +} + +// UpdateMany executes an update command to update documents in the collection. +func (dao *Mail) UpdateMany(ctx context.Context, filterFunc MailFilterFunc, updateFunc MailUpdateFunc, optionsFunc ...MailUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateMany(ctx, filter, update, opts) +} + +// FindOne executes a find command and returns a model for one document in the collection. +func (dao *Mail) FindOne(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailFindOneOptionsFunc) (*modelpkg.Mail, error) { + var ( + opts *options.FindOneOptions + model = &modelpkg.Mail{} + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + err := dao.Collection.FindOne(ctx, filter, opts).Decode(model) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + return model, nil +} + +// FindOneByID executes a find command and returns a model for one document in the collection. +func (dao *Mail) FindOneByID(ctx context.Context, id string, optionsFunc ...MailFindOneOptionsFunc) (*modelpkg.Mail, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.FindOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// FindMany executes a find command and returns many models the matching documents in the collection. +func (dao *Mail) FindMany(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailFindManyOptionsFunc) ([]*modelpkg.Mail, error) { + var ( + opts *options.FindOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + cur, err := dao.Collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + + models := make([]*modelpkg.Mail, 0) + + if err = cur.All(ctx, &models); err != nil { + return nil, err + } + + return models, nil +} + +// DeleteOne executes a delete command to delete at most one document from the collection. +func (dao *Mail) DeleteOne(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteOne(ctx, filter, opts) +} + +// DeleteOneByID executes a delete command to delete at most one document from the collection. +func (dao *Mail) DeleteOneByID(ctx context.Context, id string, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.DeleteOne(ctx, func(cols *MailColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// DeleteMany executes a delete command to delete documents from the collection. +func (dao *Mail) DeleteMany(ctx context.Context, filterFunc MailFilterFunc, optionsFunc ...MailDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteMany(ctx, filter, opts) +} + +// autofill when inserting data +func (dao *Mail) autofill(ctx context.Context, model *modelpkg.Mail) error { + if model.ID.IsZero() { + model.ID = primitive.NewObjectID() + } + + if model.SendTime == 0 { + model.SendTime = primitive.NewDateTimeFromTime(time.Now()) + } + + return nil +} diff --git a/core/tools/mongoctl/example/dao/internal/user.go b/core/tools/mongoctl/example/dao/internal/user.go new file mode 100644 index 0000000..28eb4a4 --- /dev/null +++ b/core/tools/mongoctl/example/dao/internal/user.go @@ -0,0 +1,330 @@ +// -------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------- + +package internal + +import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + modelpkg "mongo.games.com/goserver/mongoctl/example/model" + "time" +) + +type UserFilterFunc func(cols *UserColumns) interface{} +type UserUpdateFunc func(cols *UserColumns) interface{} +type UserPipelineFunc func(cols *UserColumns) interface{} +type UserCountOptionsFunc func(cols *UserColumns) *options.CountOptions +type UserAggregateOptionsFunc func(cols *UserColumns) *options.AggregateOptions +type UserFindOneOptionsFunc func(cols *UserColumns) *options.FindOneOptions +type UserFindManyOptionsFunc func(cols *UserColumns) *options.FindOptions +type UserUpdateOptionsFunc func(cols *UserColumns) *options.UpdateOptions +type UserDeleteOptionsFunc func(cols *UserColumns) *options.DeleteOptions +type UserInsertOneOptionsFunc func(cols *UserColumns) *options.InsertOneOptions +type UserInsertManyOptionsFunc func(cols *UserColumns) *options.InsertManyOptions + +type User struct { + Columns *UserColumns + Database *mongo.Database + Collection *mongo.Collection +} + +type UserColumns struct { + ID string + UID string // 用户ID + Account string // 用户账号 + Password string // 用户密码 + Salt string // 密码 + Mobile string // 用户手机 + Email string // 用户邮箱 + Nickname string // 用户昵称 + Signature string // 用户签名 + Gender string // 用户性别 + Level string // 用户等级 + Experience string // 用户经验 + Coin string // 用户金币 + Type string // 用户类型 + Status string // 用户状态 + DeviceID string // 设备ID + ThirdPlatforms string // 第三方平台 + RegisterIP string // 注册IP + RegisterTime string // 注册时间 + LastLoginIP string // 最近登录IP + LastLoginTime string // 最近登录时间 +} + +var userColumns = &UserColumns{ + ID: "_id", + UID: "uid", // 用户ID + Account: "account", // 用户账号 + Password: "password", // 用户密码 + Salt: "salt", // 密码 + Mobile: "mobile", // 用户手机 + Email: "email", // 用户邮箱 + Nickname: "nickname", // 用户昵称 + Signature: "signature", // 用户签名 + Gender: "gender", // 用户性别 + Level: "level", // 用户等级 + Experience: "experience", // 用户经验 + Coin: "coin", // 用户金币 + Type: "type", // 用户类型 + Status: "status", // 用户状态 + DeviceID: "device_id", // 设备ID + ThirdPlatforms: "third_platforms", // 第三方平台 + RegisterIP: "register_ip", // 注册IP + RegisterTime: "register_time", // 注册时间 + LastLoginIP: "last_login_ip", // 最近登录IP + LastLoginTime: "last_login_time", // 最近登录时间 +} + +func NewUser(db *mongo.Database) *User { + return &User{ + Columns: userColumns, + Database: db, + Collection: db.Collection("user"), + } +} + +// Count returns the number of documents in the collection. +func (dao *User) Count(ctx context.Context, filterFunc UserFilterFunc, optionsFunc ...UserCountOptionsFunc) (int64, error) { + var ( + opts *options.CountOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.CountDocuments(ctx, filter, opts) +} + +// Aggregate executes an aggregate command against the collection and returns a cursor over the resulting documents. +func (dao *User) Aggregate(ctx context.Context, pipelineFunc UserPipelineFunc, optionsFunc ...UserAggregateOptionsFunc) (*mongo.Cursor, error) { + var ( + opts *options.AggregateOptions + pipeline = pipelineFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.Aggregate(ctx, pipeline, opts) +} + +// InsertOne executes an insert command to insert a single document into the collection. +func (dao *User) InsertOne(ctx context.Context, model *modelpkg.User, optionsFunc ...UserInsertOneOptionsFunc) (*mongo.InsertOneResult, error) { + if model == nil { + return nil, errors.New("model is nil") + } + + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + + var opts *options.InsertOneOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertOne(ctx, model, opts) +} + +// InsertMany executes an insert command to insert multiple documents into the collection. +func (dao *User) InsertMany(ctx context.Context, models []*modelpkg.User, optionsFunc ...UserInsertManyOptionsFunc) (*mongo.InsertManyResult, error) { + if len(models) == 0 { + return nil, errors.New("models is empty") + } + + documents := make([]interface{}, 0, len(models)) + for i := range models { + model := models[i] + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + documents = append(documents, model) + } + + var opts *options.InsertManyOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertMany(ctx, documents, opts) +} + +// UpdateOne executes an update command to update at most one document in the collection. +func (dao *User) UpdateOne(ctx context.Context, filterFunc UserFilterFunc, updateFunc UserUpdateFunc, optionsFunc ...UserUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateOne(ctx, filter, update, opts) +} + +// UpdateOneByID executes an update command to update at most one document in the collection. +func (dao *User) UpdateOneByID(ctx context.Context, id string, updateFunc UserUpdateFunc, optionsFunc ...UserUpdateOptionsFunc) (*mongo.UpdateResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.UpdateOne(ctx, func(cols *UserColumns) interface{} { + return bson.M{"_id": objectID} + }, updateFunc, optionsFunc...) +} + +// UpdateMany executes an update command to update documents in the collection. +func (dao *User) UpdateMany(ctx context.Context, filterFunc UserFilterFunc, updateFunc UserUpdateFunc, optionsFunc ...UserUpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateMany(ctx, filter, update, opts) +} + +// FindOne executes a find command and returns a model for one document in the collection. +func (dao *User) FindOne(ctx context.Context, filterFunc UserFilterFunc, optionsFunc ...UserFindOneOptionsFunc) (*modelpkg.User, error) { + var ( + opts *options.FindOneOptions + model = &modelpkg.User{} + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + err := dao.Collection.FindOne(ctx, filter, opts).Decode(model) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + return model, nil +} + +// FindOneByID executes a find command and returns a model for one document in the collection. +func (dao *User) FindOneByID(ctx context.Context, id string, optionsFunc ...UserFindOneOptionsFunc) (*modelpkg.User, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.FindOne(ctx, func(cols *UserColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// FindMany executes a find command and returns many models the matching documents in the collection. +func (dao *User) FindMany(ctx context.Context, filterFunc UserFilterFunc, optionsFunc ...UserFindManyOptionsFunc) ([]*modelpkg.User, error) { + var ( + opts *options.FindOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + cur, err := dao.Collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + + models := make([]*modelpkg.User, 0) + + if err = cur.All(ctx, &models); err != nil { + return nil, err + } + + return models, nil +} + +// DeleteOne executes a delete command to delete at most one document from the collection. +func (dao *User) DeleteOne(ctx context.Context, filterFunc UserFilterFunc, optionsFunc ...UserDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteOne(ctx, filter, opts) +} + +// DeleteOneByID executes a delete command to delete at most one document from the collection. +func (dao *User) DeleteOneByID(ctx context.Context, id string, optionsFunc ...UserDeleteOptionsFunc) (*mongo.DeleteResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.DeleteOne(ctx, func(cols *UserColumns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// DeleteMany executes a delete command to delete documents from the collection. +func (dao *User) DeleteMany(ctx context.Context, filterFunc UserFilterFunc, optionsFunc ...UserDeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteMany(ctx, filter, opts) +} + +// autofill when inserting data +func (dao *User) autofill(ctx context.Context, model *modelpkg.User) error { + if model.ID.IsZero() { + model.ID = primitive.NewObjectID() + } + + if model.UID == 0 { + if id, err := NewCounter(dao.Database).Incr(ctx, "uid"); err != nil { + return err + } else { + model.UID = int32(id) + } + } + + if model.RegisterTime == 0 { + model.RegisterTime = primitive.NewDateTimeFromTime(time.Now()) + } + + if model.LastLoginTime == 0 { + model.LastLoginTime = primitive.NewDateTimeFromTime(time.Now()) + } + + return nil +} diff --git a/core/tools/mongoctl/example/dao/mail.go b/core/tools/mongoctl/example/dao/mail.go new file mode 100644 index 0000000..06d2b80 --- /dev/null +++ b/core/tools/mongoctl/example/dao/mail.go @@ -0,0 +1,22 @@ +package dao + +import ( + "go.mongodb.org/mongo-driver/mongo" + "mongo.games.com/goserver/mongoctl/example/dao/internal" +) + +type MailColumns = internal.MailColumns + +type Mail struct { + *internal.Mail +} + +func NewMail(db *mongo.Database, c *mongo.Collection) *Mail { + v := internal.NewMail(nil) + v.Database = db + v.Collection = c + panic("创建索引") + //c.Indexes().CreateOne() + //c.Indexes().CreateMany() + return &Mail{Mail: v} +} diff --git a/core/tools/mongoctl/example/dao/user.go b/core/tools/mongoctl/example/dao/user.go new file mode 100644 index 0000000..81d71ed --- /dev/null +++ b/core/tools/mongoctl/example/dao/user.go @@ -0,0 +1,22 @@ +package dao + +import ( + "go.mongodb.org/mongo-driver/mongo" + "mongo.games.com/goserver/mongoctl/example/dao/internal" +) + +type UserColumns = internal.UserColumns + +type User struct { + *internal.User +} + +func NewUser(db *mongo.Database, c *mongo.Collection) *User { + v := internal.NewUser(nil) + v.Database = db + v.Collection = c + panic("创建索引") + //c.Indexes().CreateOne() + //c.Indexes().CreateMany() + return &User{User: v} +} diff --git a/core/tools/mongoctl/example/main.go b/core/tools/mongoctl/example/main.go new file mode 100644 index 0000000..b3fc63c --- /dev/null +++ b/core/tools/mongoctl/example/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "log" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" + + "mongo.games.com/goserver/mongoctl/example/dao" + "mongo.games.com/goserver/mongoctl/example/model" +) + +func main() { + var ( + uri = "mongodb://root:12345678@127.0.0.1:27017" + opts = options.Client().ApplyURI(uri) + baseCtx = context.Background() + ) + + ctx, cancel := context.WithTimeout(baseCtx, 5*time.Second) + client, err := mongo.Connect(ctx, opts) + cancel() + if err != nil { + log.Fatalf("connect mongo server failed: %v", err) + } + + ctx, cancel = context.WithTimeout(baseCtx, 5*time.Second) + defer cancel() + err = client.Ping(ctx, readpref.Primary()) + cancel() + if err != nil { + log.Fatalf("ping mongo server failed: %v", err) + } + + db := client.Database("dao_test") + + mailDao := dao.NewMail(db, db.Collection("mail")) + + _, err = mailDao.InsertOne(baseCtx, &model.Mail{ + Title: "mongo-dao-generator introduction", + Content: "the mongo-dao-generator is a tool for automatically generating MongoDB Data Access Object.", + Sender: 1, + Receiver: 2, + Status: 1, + }) + if err != nil { + log.Fatalf("failed to insert into mongo database: %v", err) + } + + mail, err := mailDao.FindOne(baseCtx, func(cols *dao.MailColumns) interface{} { + return bson.M{cols.Receiver: 2} + }) + if err != nil { + log.Fatalf("failed to find a row of data from mongo database: %v", err) + } + + log.Printf("%+v", mail) +} diff --git a/core/tools/mongoctl/example/model/mail.go b/core/tools/mongoctl/example/model/mail.go new file mode 100644 index 0000000..c5a65e4 --- /dev/null +++ b/core/tools/mongoctl/example/model/mail.go @@ -0,0 +1,16 @@ +package model + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +//go:generate mongoctl -model-dir=. -model-names=Mail -dao-dir=../dao/ +type Mail struct { + ID primitive.ObjectID `bson:"_id" gen:"autoFill"` // 邮件ID + Title string `bson:"title"` // 邮件标题 + Content string `bson:"content"` // 邮件内容 + Sender int64 `bson:"sender"` // 邮件发送者 + Receiver int64 `bson:"receiver"` // 邮件接受者 + Status int `bson:"status"` // 邮件状态 + SendTime primitive.DateTime `bson:"send_time" gen:"autoFill"` // 发送时间 +} diff --git a/core/tools/mongoctl/example/model/user.go b/core/tools/mongoctl/example/model/user.go new file mode 100644 index 0000000..830bbbb --- /dev/null +++ b/core/tools/mongoctl/example/model/user.go @@ -0,0 +1,61 @@ +package model + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type Gender int + +const ( + GenderUnknown Gender = iota // 未知 + GenderMale // 男性 + GenderFemale // 女性 +) + +// Type 用户类型 +type Type int + +const ( + TypeRobot Type = 0 // 机器人用户 + TypeGuest Type = 1 // 游客用户 + TypeGeneral Type = 2 // 普通用户 + TypeSystem Type = 3 // 系统用户 +) + +// Status 用户状态 +type Status int + +const ( + StatusNormal Status = iota // 正常 + StatusForbidden // 封禁 +) + +//go:generate mongoctl -model-dir=. -model-names=User -dao-dir=../dao/ +type User struct { + ID primitive.ObjectID `bson:"_id" gen:"autoFill"` + UID int32 `bson:"uid" gen:"autoIncr:uid"` // 用户ID + Account string `bson:"account"` // 用户账号 + Password string `bson:"password"` // 用户密码 + Salt string `bson:"salt"` // 密码 + Mobile string `bson:"mobile"` // 用户手机 + Email string `bson:"email"` // 用户邮箱 + Nickname string `bson:"nickname"` // 用户昵称 + Signature string `bson:"signature"` // 用户签名 + Gender Gender `bson:"gender"` // 用户性别 + Level int `bson:"level"` // 用户等级 + Experience int `bson:"experience"` // 用户经验 + Coin int `bson:"coin"` // 用户金币 + Type Type `bson:"type"` // 用户类型 + Status Status `bson:"status"` // 用户状态 + DeviceID string `bson:"device_id"` // 设备ID + ThirdPlatforms ThirdPlatforms `bson:"third_platforms"` // 第三方平台 + RegisterIP string `bson:"register_ip"` // 注册IP + RegisterTime primitive.DateTime `bson:"register_time" gen:"autoFill"` // 注册时间 + LastLoginIP string `bson:"last_login_ip"` // 最近登录IP + LastLoginTime primitive.DateTime `bson:"last_login_time" gen:"autoFill"` // 最近登录时间 +} + +// ThirdPlatforms 第三方平台 +type ThirdPlatforms struct { + Wechat string `bson:"wechat"` // 微信登录openid + Google string `bson:"google"` // 谷歌登录userid + Facebook string `bson:"facebook"` // 脸书登录userid +} diff --git a/core/tools/mongoctl/generator.go b/core/tools/mongoctl/generator.go new file mode 100644 index 0000000..25af672 --- /dev/null +++ b/core/tools/mongoctl/generator.go @@ -0,0 +1,395 @@ +package main + +import ( + "fmt" + "go/ast" + "go/token" + "log" + "os" + "path/filepath" + "reflect" + "strings" + + "golang.org/x/tools/go/packages" + + "mongo.games.com/goserver/mongoctl/template" +) + +const ( + symbolBacktick = "`" +) + +const ( + symbolBacktickKey = "SymbolBacktick" +) + +const ( + varPackagesKey = "VarPackages" + varModelClassNameKey = "VarModelClassName" + varModelPackageNameKey = "VarModelPackageName" + varModelPackagePathKey = "VarModelPackagePath" + varModelVariableNameKey = "VarModelVariableName" + varModelColumnsDefineKey = "VarModelColumnsDefine" + varModelColumnsInstanceKey = "VarModelColumnsInstance" + varDaoClassNameKey = "VarDaoClassName" + varDaoVariableNameKey = "VarDaoVariableName" + varDaoPackageNameKey = "VarDaoPackageName" + varDaoPackagePathKey = "VarDaoPackagePath" + varDaoPrefixNameKey = "VarDaoPrefixName" + varCollectionNameKey = "VarCollectionName" + varAutofillCodeKey = "VarAutofillCode" +) + +const defaultCounterName = "Counter" + +type options struct { + modelDir string // + modelPkgPath string // + modelPkgAlias string // + modelNames []string // + daoDir string + daoPkgPath string + subPkgEnable bool + subPkgStyle style + counterName string + fileNameStyle style +} + +type generator struct { + opts *options + counter *counter + modelNames map[string]struct{} +} + +func newGenerator(opts *options) *generator { + modelNames := make(map[string]struct{}, len(opts.modelNames)) + for _, modelName := range opts.modelNames { + if isExportable(modelName) { // 是否导出字段 + modelNames[modelName] = struct{}{} + } + } + + if len(modelNames) == 0 { + log.Fatalf("error: %d model type names found", len(modelNames)) + } + + if opts.counterName == "" { + opts.counterName = defaultCounterName + } + + return &generator{ + opts: opts, + counter: newCounter(opts), + modelNames: modelNames, + } +} + +func (g *generator) makeDao() { + models := g.parseModels() + + for _, m := range models { + g.makeModelInternalDao(m) + + g.makeModelExternalDao(m) + + fmt.Printf("%s's dao file generated successfully\n", m.modelName) + + if !m.isDependCounter { + continue + } + + g.makeCounterInternalDao() + + g.makeCounterExternalDao() + } +} + +// generate an internal dao file based on model +func (g *generator) makeModelInternalDao(m *model) { + replaces := make(map[string]string) + replaces[varModelClassNameKey] = m.modelClassName + replaces[varModelPackageNameKey] = m.modelPkgName + replaces[varModelPackagePathKey] = m.modelPkgPath + replaces[varModelVariableNameKey] = m.modelVariableName + replaces[varDaoPrefixNameKey] = m.daoPrefixName + replaces[varDaoClassNameKey] = m.daoClassName + replaces[varDaoVariableNameKey] = m.daoVariableName + replaces[varCollectionNameKey] = m.collectionName + replaces[varModelColumnsDefineKey] = m.modelColumnsDefined() + replaces[varModelColumnsInstanceKey] = m.modelColumnsInstance() + replaces[varAutofillCodeKey] = m.autoFillCode() + replaces[varPackagesKey] = m.packages() + + file := m.daoOutputDir + "/internal/" + m.daoOutputFile + + err := doWrite(file, template.InternalTemplate, replaces) + if err != nil { + log.Fatal(err) + } +} + +// generate an external dao file based on model +func (g *generator) makeModelExternalDao(m *model) { + file := m.daoOutputDir + "/" + m.daoOutputFile + + _, err := os.Stat(file) + if err != nil { + switch { + case os.IsNotExist(err): + // ignore + case os.IsExist(err): + return + default: + log.Fatal(err) + } + } else { + return + } + + replaces := make(map[string]string) + replaces[varDaoClassNameKey] = m.daoClassName + replaces[varDaoPrefixNameKey] = m.daoPrefixName + replaces[varDaoPackageNameKey] = m.daoPkgName + replaces[varDaoPackagePathKey] = m.daoPkgPath + + err = doWrite(file, template.ExternalTemplate, replaces) + if err != nil { + log.Fatal(err) + } +} + +// generate an internal dao file based on counter model +func (g *generator) makeCounterInternalDao() { + replaces := make(map[string]string) + replaces[varDaoClassNameKey] = g.counter.daoClassName + replaces[varDaoPrefixNameKey] = g.counter.daoPrefixName + replaces[varDaoVariableNameKey] = g.counter.daoVariableName + replaces[varCollectionNameKey] = g.counter.collectionName + replaces[symbolBacktickKey] = symbolBacktick + + file := g.counter.daoOutputDir + "/internal/" + g.counter.daoOutputFile + + err := doWrite(file, template.CounterInternalTemplate, replaces) + if err != nil { + log.Fatal(err) + } +} + +// generate an external dao file based on counter model +func (g *generator) makeCounterExternalDao() { + file := g.counter.daoOutputDir + "/" + g.counter.daoOutputFile + + _, err := os.Stat(file) + if err != nil { + switch { + case os.IsNotExist(err): + // ignore + case os.IsExist(err): + return + default: + log.Fatal(err) + } + } else { + return + } + + replaces := make(map[string]string) + replaces[varDaoClassNameKey] = g.counter.daoClassName + replaces[varDaoPackageNameKey] = g.counter.daoPkgName + replaces[varDaoPackagePathKey] = g.counter.daoPkgPath + + err = doWrite(file, template.CounterExternalTemplate, replaces) + if err != nil { + log.Fatal(err) + } +} + +// parse multiple models from the go file +func (g *generator) parseModels() []*model { + var ( + pkg = g.loadPackage() + models = make([]*model, 0, len(pkg.Syntax)) + daoPkgPath = g.opts.daoPkgPath + modelPkgPath = g.opts.modelPkgPath + modelPkgName = g.opts.modelPkgAlias + ) + + if g.opts.daoPkgPath == "" && pkg.Module != nil { + outPath, err := filepath.Abs(g.opts.daoDir) + if err != nil { + log.Fatal(err) + } + daoPkgPath = pkg.Module.Path + outPath[len(pkg.Module.Dir):] + } + + daoPkgPath = strings.ReplaceAll(daoPkgPath, `\`, `/`) + + g.counter.setDaoPkgPath(daoPkgPath) + + for _, file := range pkg.Syntax { + if g.opts.modelPkgPath == "" && pkg.Module != nil && pkg.Fset != nil { + filePath := filepath.Dir(pkg.Fset.Position(file.Package).Filename) + modelPkgPath = pkg.Module.Path + filePath[len(pkg.Module.Dir):] + } + + modelPkgPath = strings.ReplaceAll(modelPkgPath, `\`, `/`) + modelPkgName = file.Name.Name + + ast.Inspect(file, func(node ast.Node) bool { + decl, ok := node.(*ast.GenDecl) + if !ok || decl.Tok != token.TYPE { + return true + } + + for _, s := range decl.Specs { + spec, ok := s.(*ast.TypeSpec) + if !ok { + continue + } + + _, ok = g.modelNames[spec.Name.Name] + if !ok { + continue + } + + st, ok := spec.Type.(*ast.StructType) + if !ok { + continue + } + + model := newModel(g.opts) + model.setModelName(spec.Name.Name) + model.setModelPkg(modelPkgName, modelPkgPath) + model.setDaoPkgPath(daoPkgPath) + + for _, item := range st.Fields.List { + name := item.Names[0].Name + + if !isExportable(name) { + continue + } + + field := &field{name: name, column: name} + + if item.Tag != nil && len(item.Tag.Value) > 2 { + runes := []rune(item.Tag.Value) + if runes[0] != '`' || runes[len(runes)-1] != '`' { + continue + } + + tag := reflect.StructTag(runes[1 : len(runes)-1]) + + if column := tag.Get("bson"); column != "" { + field.column = column + } + + val, ok := tag.Lookup("gen") + if ok { + parts := strings.Split(val, ";") + for _, part := range parts { + if part == "" { + continue + } + + switch eles := strings.SplitN(part, ":", 2); eles[0] { + case "autoFill": + expr, ok := item.Type.(*ast.SelectorExpr) + if !ok { + continue + } + + switch fmt.Sprintf("%s.%s", expr.X.(*ast.Ident).Name, expr.Sel.Name) { + case "primitive.ObjectID": + field.autoFill = objectID + model.addImport(pkg3) + case "primitive.DateTime": + field.autoFill = dateTime + model.addImport(pkg1) + model.addImport(pkg3) + } + case "autoIncr": + if len(eles) != 2 || eles[1] == "" { + continue + } + + expr, ok := item.Type.(*ast.Ident) + if !ok { + continue + } + + switch expr.Name { + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + field.autoFill = autoIncr + field.autoIncrFieldName = eles[1] + + if g.opts.subPkgEnable { + model.addImport(g.counter.daoPkgPath) + } + + switch expr.Name { + case "int": + field.autoIncrFieldKind = reflect.Int + case "int8": + field.autoIncrFieldKind = reflect.Int8 + case "int16": + field.autoIncrFieldKind = reflect.Int16 + case "int32": + field.autoIncrFieldKind = reflect.Int32 + case "int64": + field.autoIncrFieldKind = reflect.Int64 + case "uint": + field.autoIncrFieldKind = reflect.Uint + case "uint8": + field.autoIncrFieldKind = reflect.Uint8 + case "uint16": + field.autoIncrFieldKind = reflect.Uint16 + case "uint32": + field.autoIncrFieldKind = reflect.Uint32 + case "uint64": + field.autoIncrFieldKind = reflect.Uint64 + } + } + } + } + } + } + + if item.Doc != nil { + field.documents = make([]string, 0, len(item.Doc.List)) + for _, doc := range item.Doc.List { + field.documents = append(field.documents, doc.Text) + } + } + + if item.Comment != nil { + field.comment = item.Comment.List[0].Text + } + + model.addFields(field) + } + + models = append(models, model) + } + + return true + }) + } + + return models +} + +func (g *generator) loadPackage() *packages.Package { + cfg := &packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedModule, + Tests: false, + } + pkgs, err := packages.Load(cfg, g.opts.modelDir) + if err != nil { + log.Fatal(err) + } + if len(pkgs) != 1 { + log.Fatalf("error: %d packages found", len(pkgs)) + } + + return pkgs[0] +} diff --git a/core/tools/mongoctl/go.mod b/core/tools/mongoctl/go.mod new file mode 100644 index 0000000..86e491c --- /dev/null +++ b/core/tools/mongoctl/go.mod @@ -0,0 +1,22 @@ +module mongo.games.com/goserver/mongoctl + +go 1.22.5 + +require ( + go.mongodb.org/mongo-driver v1.17.1 + golang.org/x/tools v0.28.0 +) + +require ( + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.17.0 // indirect +) diff --git a/core/tools/mongoctl/go.sum b/core/tools/mongoctl/go.sum new file mode 100644 index 0000000..153c9fc --- /dev/null +++ b/core/tools/mongoctl/go.sum @@ -0,0 +1,54 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/core/tools/mongoctl/main.go b/core/tools/mongoctl/main.go new file mode 100644 index 0000000..33058ff --- /dev/null +++ b/core/tools/mongoctl/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strings" +) + +var ( + modelDir = flag.String("model-dir", "", "模型所在目录; 必需") + modelNames = flag.String("model-names", "", "模型结构体名称; 必需") + modelPkgPath = flag.String("model-pkg-path", "", "模型包名; 默认自动生成") + modelPkgAlias = flag.String("model-pkg-alias", "", "模型包别名") + daoDir = flag.String("dao-dir", "", "生成代码所在目录; 必需") + daoPkgPath = flag.String("dao-pkg-path", "", "生成代码所在目录的包名; 默认自动生成") + subPkgEnable = flag.Bool("sub-pkg-enable", false, "每个模型创建一个子包; 默认关闭") + subPkgStyle = flag.String("sub-pkg-style", "kebab", "模型子包目录名称风格; 选项: kebab | underscore | lower | camel | pascal; 默认是 kebab") + counterName = flag.String("counter-name", "", "自增模型名称; 默认是 counter") + fileNameStyle = flag.String("file-style", "underscore", "代码文件名称风格; 选项: kebab | underscore | lower | camel | pascal; 默认是 underscore") +) + +// Usage is a replacement usage function for the flags package. +func usage() { + fmt.Fprintf(os.Stderr, "用法 mongoctl:\n") + fmt.Fprintf(os.Stderr, "\tmongoctl [flags] -model-dir=. -model-names=T,T -dao-dir=./dao\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() +} + +//go:generate mongoctl -type=Mail,User +func main() { + log.SetFlags(0) + log.SetPrefix("mongoctl: ") + flag.Usage = usage + flag.Parse() + + if len(*modelDir) == 0 { + flag.Usage() + os.Exit(2) + } + + if len(*modelNames) == 0 { + flag.Usage() + os.Exit(2) + } + + if len(*daoDir) == 0 { + flag.Usage() + os.Exit(2) + } + + newGenerator(&options{ + daoDir: *daoDir, + daoPkgPath: *daoPkgPath, + modelDir: *modelDir, + modelNames: strings.Split(*modelNames, ","), + modelPkgPath: *modelPkgPath, + modelPkgAlias: *modelPkgAlias, + subPkgEnable: *subPkgEnable, + subPkgStyle: style(*subPkgStyle), + counterName: *counterName, + fileNameStyle: style(*fileNameStyle), + }).makeDao() +} diff --git a/core/tools/mongoctl/model.go b/core/tools/mongoctl/model.go new file mode 100644 index 0000000..f3a8c5c --- /dev/null +++ b/core/tools/mongoctl/model.go @@ -0,0 +1,273 @@ +package main + +import ( + "fmt" + "path/filepath" + "reflect" + "sort" + "strings" +) + +const ( + defaultModelPkgAlias = "modelpkg" + defaultModelVariableName = "model" +) + +type autoFill int + +const ( + objectID autoFill = iota + 1 // primitive.NewObjectID() + dateTime // primitive.NewDateTimeFromTime(time.Now()) + autoIncr // auto-increment +) + +const ( + pkg1 = "time" + pkg2 = "context" + pkg3 = "go.mongodb.org/mongo-driver/bson/primitive" + pkg4 = "go.mongodb.org/mongo-driver/mongo" + pkg5 = "go.mongodb.org/mongo-driver/mongo/options" + pkg6 = "errors" + pkg7 = "go.mongodb.org/mongo-driver/bson" +) + +type field struct { + name string + column string + comment string + documents []string + autoFill autoFill + autoIncrFieldName string + autoIncrFieldKind reflect.Kind +} + +type model struct { + opts *options + fields []*field + imports map[string]string + modelName string + modelClassName string + modelVariableName string + modelPkgPath string + modelPkgName string + daoClassName string + daoVariableName string + daoPkgPath string + daoPkgName string + daoOutputDir string + daoOutputFile string + daoPrefixName string + collectionName string + fieldNameMaxLen int + fieldComplexMaxLen int + isDependCounter bool +} + +func newModel(opts *options) *model { + m := &model{ + opts: opts, + fields: make([]*field, 0), + imports: make(map[string]string, 8), + } + + m.addImport(pkg2) + m.addImport(pkg4) + m.addImport(pkg5) + m.addImport(pkg6) + m.addImport(pkg7) + + return m +} + +func (m *model) setModelName(name string) { + m.modelName = name + m.modelClassName = toPascalCase(m.modelName) + m.modelVariableName = toCamelCase(m.modelName) + m.daoClassName = toPascalCase(m.modelName) + m.daoVariableName = toCamelCase(m.modelName) + m.daoOutputFile = fmt.Sprintf("%s.go", toFileName(m.modelName, m.opts.fileNameStyle)) + m.collectionName = toUnderscoreCase(m.modelName) + + dir := strings.TrimSuffix(m.opts.daoDir, "/") + + if m.opts.subPkgEnable { + m.daoOutputDir = dir + "/" + toPackagePath(m.modelName, m.opts.subPkgStyle) + } else { + m.daoOutputDir = dir + m.daoPrefixName = toPascalCase(m.modelName) + } +} + +func (m *model) setModelPkg(name, path string) { + m.modelPkgPath = path + + if m.opts.modelPkgAlias != "" { + m.modelPkgName = m.opts.modelPkgAlias + m.addImport(m.modelPkgPath, m.modelPkgName) + } else { + m.modelPkgName = name + m.addImport(m.modelPkgPath) + } + + if m.modelPkgName == defaultModelVariableName { + m.modelPkgName = defaultModelPkgAlias + m.addImport(m.modelPkgPath, m.modelPkgName) + } +} + +func (m *model) setDaoPkgPath(path string) { + if m.opts.subPkgEnable { + m.daoPkgPath = path + "/" + toPackagePath(m.modelName, m.opts.subPkgStyle) + } else { + m.daoPkgPath = path + } + + m.daoPkgName = toPackageName(filepath.Base(m.daoPkgPath)) +} + +func (m *model) addImport(pkg string, alias ...string) { + if len(alias) > 0 { + m.imports[pkg] = alias[0] + } else { + m.imports[pkg] = "" + } +} + +func (m *model) addFields(fields ...*field) { + for _, f := range fields { + if l := len(f.name); l > m.fieldNameMaxLen { + m.fieldNameMaxLen = l + } + + if l := len(f.name) + len(f.column) + 5; l > m.fieldComplexMaxLen { + m.fieldComplexMaxLen = l + } + + if f.autoFill == autoIncr { + m.isDependCounter = true + } + } + + m.fields = append(m.fields, fields...) +} + +func (m *model) modelColumnsDefined() (str string) { + for i, f := range m.fields { + str += fmt.Sprintf("\t%s%s%s %s", f.name, strings.Repeat(" ", m.fieldNameMaxLen-len(f.name)+1), "string", f.comment) + if i != len(m.fields)-1 { + str += "\n" + } + } + + str = strings.TrimPrefix(str, "\t") + return +} + +func (m *model) modelColumnsInstance() (str string) { + for i, f := range m.fields { + s := fmt.Sprintf("%s:%s\"%s\",", f.name, strings.Repeat(" ", m.fieldNameMaxLen-len(f.name)+1), f.column) + s += strings.Repeat(" ", m.fieldComplexMaxLen-len(s)+1) + f.comment + str += "\t" + s + if i != len(m.fields)-1 { + str += "\n" + } + } + + str = strings.TrimLeft(str, "\t") + return +} + +func (m *model) packages() (str string) { + packages := make([]string, 0, len(m.imports)) + for pkg := range m.imports { + packages = append(packages, pkg) + } + + sort.Slice(packages, func(i, j int) bool { + return packages[i] < packages[j] + }) + + for _, pkg := range packages { + if alias := m.imports[pkg]; alias != "" { + str += fmt.Sprintf("\t%s \"%s\"\n", alias, pkg) + } else { + str += fmt.Sprintf("\t\"%s\"\n", pkg) + } + } + + str = strings.TrimPrefix(str, "\t") + str = strings.TrimSuffix(str, "\n") + return +} + +func (m *model) autoFillCode() (str string) { + var ( + counterName = toPascalCase(m.opts.counterName) + counterPkgPrefix string + ) + + if m.opts.subPkgEnable { + counterPkgPrefix = fmt.Sprintf("%s.", toPackageName(counterName)) + } + + for _, f := range m.fields { + if f.autoFill == 0 { + continue + } + + if str != "" { + str += "\n\n" + } + + switch f.autoFill { + case objectID: + str += fmt.Sprintf("\tif model.%s.IsZero() {\n", f.name) + str += fmt.Sprintf("\t\tmodel.%s = primitive.NewObjectID()\n", f.name) + str += "\t}" + case dateTime: + str += fmt.Sprintf("\tif model.%s == 0 {\n", f.name) + str += fmt.Sprintf("\t\tmodel.%s = primitive.NewDateTimeFromTime(time.Now())\n", f.name) + str += "\t}" + case autoIncr: + str += fmt.Sprintf("\tif model.%s == 0 {\n", f.name) + str += fmt.Sprintf("\t\tif id, err := %sNew%s(dao.Database).Incr(ctx, \"%s\"); err != nil {\n", counterPkgPrefix, counterName, f.autoIncrFieldName) + str += "\t\t\treturn err\n" + str += "\t\t} else {\n" + + switch f.autoIncrFieldKind { + case reflect.Int: + str += fmt.Sprintf("\t\t\tmodel.%s = int(id)\n", f.name) + case reflect.Int8: + str += fmt.Sprintf("\t\t\tmodel.%s = int8(id)\n", f.name) + case reflect.Int16: + str += fmt.Sprintf("\t\t\tmodel.%s = int16(id)\n", f.name) + case reflect.Int32: + str += fmt.Sprintf("\t\t\tmodel.%s = int32(id)\n", f.name) + case reflect.Int64: + str += fmt.Sprintf("\t\t\tmodel.%s = id\n", f.name) + case reflect.Uint: + str += fmt.Sprintf("\t\t\tmodel.%s = uint(id)\n", f.name) + case reflect.Uint8: + str += fmt.Sprintf("\t\t\tmodel.%s = uint8(id)\n", f.name) + case reflect.Uint16: + str += fmt.Sprintf("\t\t\tmodel.%s = uint16(id)\n", f.name) + case reflect.Uint32: + str += fmt.Sprintf("\t\t\tmodel.%s = uint32(id)\n", f.name) + case reflect.Uint64: + str += fmt.Sprintf("\t\t\tmodel.%s = uint64(id)\n", f.name) + } + + str += "\t\t}\n" + str += "\t}" + } + } + + if str != "" { + str += "\n\n" + } + + str += "\treturn nil" + str = strings.TrimPrefix(str, "\t") + + return +} diff --git a/core/tools/mongoctl/template/counter.go b/core/tools/mongoctl/template/counter.go new file mode 100644 index 0000000..b88724d --- /dev/null +++ b/core/tools/mongoctl/template/counter.go @@ -0,0 +1,97 @@ +package template + +const CounterExternalTemplate = ` +package ${VarDaoPackageName} + +import ( + "go.mongodb.org/mongo-driver/mongo" + "${VarDaoPackagePath}/internal" +) + +type ${VarDaoClassName} struct { + *internal.${VarDaoClassName} +} + +func New${VarDaoClassName}(db *mongo.Database) *${VarDaoClassName} { + return &${VarDaoClassName}{${VarDaoClassName}: internal.New${VarDaoClassName}(db)} +} +` + +const CounterInternalTemplate = ` +// -------------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------------- + +package internal + +import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ${VarDaoClassName} struct { + Columns *${VarDaoPrefixName}Columns + Database *mongo.Database + Collection *mongo.Collection +} + +type ${VarDaoPrefixName}Model struct { + ID string ${SymbolBacktick}bson:"_id"${SymbolBacktick} + Value int64 ${SymbolBacktick}bson:"value"${SymbolBacktick} +} + +type ${VarDaoPrefixName}Columns struct { + ID string + Value string +} + +var ${VarDaoVariableName}Columns = &${VarDaoPrefixName}Columns{ + ID: "_id", + Value: "value", +} + +func New${VarDaoClassName}(db *mongo.Database) *${VarDaoClassName} { + return &${VarDaoClassName}{ + Columns: ${VarDaoVariableName}Columns, + Database: db, + Collection: db.Collection("${VarCollectionName}"), + } +} + +// Incr 自增值 +func (dao *${VarDaoClassName}) Incr(ctx context.Context, key string, incr ...int) (int64, error) { + var ( + upsert = true + returnDocument = options.After + counter = &${VarDaoPrefixName}Model{} + value = 1 + ) + + if len(incr) > 0 { + if incr[0] == 0 { + return 0, errors.New("invalid increment value") + } + value = incr[0] + } + + rst := dao.Collection.FindOneAndUpdate(ctx, bson.M{ + dao.Columns.ID: key, + }, bson.M{"$inc": bson.M{ + dao.Columns.Value: value, + }}, &options.FindOneAndUpdateOptions{ + Upsert: &upsert, + ReturnDocument: &returnDocument, + }) + + if err := rst.Decode(counter); err != nil { + return 0, err + } + + return counter.Value, nil +} +` diff --git a/core/tools/mongoctl/template/template.go b/core/tools/mongoctl/template/template.go new file mode 100644 index 0000000..358aeb8 --- /dev/null +++ b/core/tools/mongoctl/template/template.go @@ -0,0 +1,292 @@ +package template + +const ExternalTemplate = ` +package ${VarDaoPackageName} + +import ( + "go.mongodb.org/mongo-driver/mongo" + "${VarDaoPackagePath}/internal" +) + +type ${VarDaoPrefixName}Columns = internal.${VarDaoPrefixName}Columns + +type ${VarDaoClassName} struct { + *internal.${VarDaoClassName} +} + +func New${VarDaoClassName}(db *mongo.Database, c *mongo.Collection) *${VarDaoClassName} { + v := internal.New${VarDaoClassName}(nil) + v.Database = db + v.Collection = c + panic("创建索引") + //c.Indexes().CreateOne() + //c.Indexes().CreateMany() + return &${VarDaoClassName}{${VarDaoClassName}: v} +} +` + +const InternalTemplate = ` +// -------------------------------------------------------------------------------------------- +// The following code is automatically generated by the mongo-dao-generator tool. +// Please do not modify this code manually to avoid being overwritten in the next generation. +// For more tool details, please click the link to view https://github.com/dobyte/mongo-dao-generator +// -------------------------------------------------------------------------------------------- + +package internal + +import ( + ${VarPackages} +) + +type ${VarDaoPrefixName}FilterFunc func(cols *${VarDaoPrefixName}Columns) interface{} +type ${VarDaoPrefixName}UpdateFunc func(cols *${VarDaoPrefixName}Columns) interface{} +type ${VarDaoPrefixName}PipelineFunc func(cols *${VarDaoPrefixName}Columns) interface{} +type ${VarDaoPrefixName}CountOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.CountOptions +type ${VarDaoPrefixName}AggregateOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.AggregateOptions +type ${VarDaoPrefixName}FindOneOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.FindOneOptions +type ${VarDaoPrefixName}FindManyOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.FindOptions +type ${VarDaoPrefixName}UpdateOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.UpdateOptions +type ${VarDaoPrefixName}DeleteOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.DeleteOptions +type ${VarDaoPrefixName}InsertOneOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.InsertOneOptions +type ${VarDaoPrefixName}InsertManyOptionsFunc func(cols *${VarDaoPrefixName}Columns) *options.InsertManyOptions + +type ${VarDaoClassName} struct { + Columns *${VarDaoPrefixName}Columns + Database *mongo.Database + Collection *mongo.Collection +} + +type ${VarDaoPrefixName}Columns struct { + ${VarModelColumnsDefine} +} + +var ${VarDaoVariableName}Columns = &${VarDaoPrefixName}Columns{ + ${VarModelColumnsInstance} +} + +func New${VarDaoClassName}(db *mongo.Database) *${VarDaoClassName} { + return &${VarDaoClassName}{ + Columns: ${VarDaoVariableName}Columns, + Database: db, + Collection: db.Collection("${VarCollectionName}"), + } +} + +// Count returns the number of documents in the collection. +func (dao *${VarDaoClassName}) Count(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, optionsFunc ...${VarDaoPrefixName}CountOptionsFunc) (int64, error) { + var ( + opts *options.CountOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.CountDocuments(ctx, filter, opts) +} + +// Aggregate executes an aggregate command against the collection and returns a cursor over the resulting documents. +func (dao *${VarDaoClassName}) Aggregate(ctx context.Context, pipelineFunc ${VarDaoPrefixName}PipelineFunc, optionsFunc ...${VarDaoPrefixName}AggregateOptionsFunc) (*mongo.Cursor, error) { + var ( + opts *options.AggregateOptions + pipeline = pipelineFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.Aggregate(ctx, pipeline, opts) +} + +// InsertOne executes an insert command to insert a single document into the collection. +func (dao *${VarDaoClassName}) InsertOne(ctx context.Context, model *${VarModelPackageName}.${VarModelClassName}, optionsFunc ...${VarDaoPrefixName}InsertOneOptionsFunc) (*mongo.InsertOneResult, error) { + if model == nil { + return nil, errors.New("model is nil") + } + + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + + var opts *options.InsertOneOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertOne(ctx, model, opts) +} + +// InsertMany executes an insert command to insert multiple documents into the collection. +func (dao *${VarDaoClassName}) InsertMany(ctx context.Context, models []*${VarModelPackageName}.${VarModelClassName}, optionsFunc ...${VarDaoPrefixName}InsertManyOptionsFunc) (*mongo.InsertManyResult, error) { + if len(models) == 0 { + return nil, errors.New("models is empty") + } + + documents := make([]interface{}, 0, len(models)) + for i := range models { + model := models[i] + if err := dao.autofill(ctx, model); err != nil { + return nil, err + } + documents = append(documents, model) + } + + var opts *options.InsertManyOptions + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.InsertMany(ctx, documents, opts) +} + +// UpdateOne executes an update command to update at most one document in the collection. +func (dao *${VarDaoClassName}) UpdateOne(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, updateFunc ${VarDaoPrefixName}UpdateFunc, optionsFunc ...${VarDaoPrefixName}UpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateOne(ctx, filter, update, opts) +} + +// UpdateOneByID executes an update command to update at most one document in the collection. +func (dao *${VarDaoClassName}) UpdateOneByID(ctx context.Context, id string, updateFunc ${VarDaoPrefixName}UpdateFunc, optionsFunc ...${VarDaoPrefixName}UpdateOptionsFunc) (*mongo.UpdateResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.UpdateOne(ctx, func(cols *${VarDaoPrefixName}Columns) interface{} { + return bson.M{"_id": objectID} + }, updateFunc, optionsFunc...) +} + +// UpdateMany executes an update command to update documents in the collection. +func (dao *${VarDaoClassName}) UpdateMany(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, updateFunc ${VarDaoPrefixName}UpdateFunc, optionsFunc ...${VarDaoPrefixName}UpdateOptionsFunc) (*mongo.UpdateResult, error) { + var ( + opts *options.UpdateOptions + filter = filterFunc(dao.Columns) + update = updateFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.UpdateMany(ctx, filter, update, opts) +} + +// FindOne executes a find command and returns a model for one document in the collection. +func (dao *${VarDaoClassName}) FindOne(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, optionsFunc ...${VarDaoPrefixName}FindOneOptionsFunc) (*${VarModelPackageName}.${VarModelClassName}, error) { + var ( + opts *options.FindOneOptions + model = &${VarModelPackageName}.${VarModelClassName}{} + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + err := dao.Collection.FindOne(ctx, filter, opts).Decode(model) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + return model, nil +} + +// FindOneByID executes a find command and returns a model for one document in the collection. +func (dao *${VarDaoClassName}) FindOneByID(ctx context.Context, id string, optionsFunc ...${VarDaoPrefixName}FindOneOptionsFunc) (*${VarModelPackageName}.${VarModelClassName}, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.FindOne(ctx, func(cols *${VarDaoPrefixName}Columns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// FindMany executes a find command and returns many models the matching documents in the collection. +func (dao *${VarDaoClassName}) FindMany(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, optionsFunc ...${VarDaoPrefixName}FindManyOptionsFunc) ([]*${VarModelPackageName}.${VarModelClassName}, error) { + var ( + opts *options.FindOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + cur, err := dao.Collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + + models := make([]*${VarModelPackageName}.${VarModelClassName}, 0) + + if err = cur.All(ctx, &models); err != nil { + return nil, err + } + + return models, nil +} + +// DeleteOne executes a delete command to delete at most one document from the collection. +func (dao *${VarDaoClassName}) DeleteOne(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, optionsFunc ...${VarDaoPrefixName}DeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteOne(ctx, filter, opts) +} + +// DeleteOneByID executes a delete command to delete at most one document from the collection. +func (dao *${VarDaoClassName}) DeleteOneByID(ctx context.Context, id string, optionsFunc ...${VarDaoPrefixName}DeleteOptionsFunc) (*mongo.DeleteResult, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + return dao.DeleteOne(ctx, func(cols *${VarDaoPrefixName}Columns) interface{} { + return bson.M{"_id": objectID} + }, optionsFunc...) +} + +// DeleteMany executes a delete command to delete documents from the collection. +func (dao *${VarDaoClassName}) DeleteMany(ctx context.Context, filterFunc ${VarDaoPrefixName}FilterFunc, optionsFunc ...${VarDaoPrefixName}DeleteOptionsFunc) (*mongo.DeleteResult, error) { + var ( + opts *options.DeleteOptions + filter = filterFunc(dao.Columns) + ) + + if len(optionsFunc) > 0 { + opts = optionsFunc[0](dao.Columns) + } + + return dao.Collection.DeleteMany(ctx, filter, opts) +} + +// autofill when inserting data +func (dao *${VarDaoClassName}) autofill(ctx context.Context, model *${VarModelPackageName}.${VarModelClassName}) error { + ${VarAutofillCode} +} +` diff --git a/core/tools/mongoctl/util.go b/core/tools/mongoctl/util.go new file mode 100644 index 0000000..64d1270 --- /dev/null +++ b/core/tools/mongoctl/util.go @@ -0,0 +1,160 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "unicode" + "unicode/utf8" +) + +type style string + +const ( + kebabCase style = "kebab" // 全小写中划线 + underscoreCase style = "underscore" // 小写下划线 + camelCase style = "camel" // 小写字母开头驼峰 + pascalCase style = "pascal" // 大写字母开头驼峰 + lowerCase style = "lower" // 全小写 +) + +// convert to underscore style, example: UserProfile > user_profile +func toUnderscoreCase(s string) string { + return toLowerCase(s, 95) +} + +// convert to kebab style, example: UserProfile > user-profile +func toKebabCase(s string) string { + return toLowerCase(s, 45) +} + +// convert to camel style, example: user-profile > userProfile +func toCamelCase(s string) string { + chars := make([]rune, 0, len(s)) + upper := false + first := true + + for i := 0; i < len(s); i++ { + switch { + case s[i] >= 65 && s[i] <= 90: + if first { + chars = append(chars, rune(s[i]+32)) + } else { + chars = append(chars, rune(s[i])) + } + first = false + upper = false + case s[i] >= 97 && s[i] <= 122: + if upper && !first { + chars = append(chars, rune(s[i]-32)) + } else { + chars = append(chars, rune(s[i])) + } + first = false + upper = false + case s[i] == 45: + upper = true + case s[i] == 95: + upper = true + } + } + + return string(chars) +} + +// convert to pascal style, example: user-profile > UserProfile +func toPascalCase(s string) string { + s = toCamelCase(s) + return strings.ToUpper(string(s[0])) + s[1:] +} + +func toLowerCase(s string, c rune) string { + chars := make([]rune, 0) + + for i := 0; i < len(s); i++ { + if s[i] >= 65 && s[i] <= 90 { + if i == 0 { + chars = append(chars, rune(s[i]+32)) + } else { + chars = append(chars, c, rune(s[i]+32)) + } + } else { + chars = append(chars, rune(s[i])) + } + } + + return string(chars) +} + +func toPackageName(s string) string { + chars := make([]rune, 0, len(s)) + for i := 0; i < len(s); i++ { + switch { + case s[i] >= 65 && s[i] <= 90: + chars = append(chars, rune(s[i]+32)) + case s[i] >= 97 && s[i] <= 122: + chars = append(chars, rune(s[i])) + } + } + + return string(chars) +} + +func toPackagePath(s string, style style) string { + switch style { + case kebabCase: + return toKebabCase(s) + case underscoreCase: + return toUnderscoreCase(s) + case camelCase: + return toCamelCase(s) + case pascalCase: + return toPascalCase(s) + case lowerCase: + return toPackageName(s) + default: + return toKebabCase(s) + } +} + +func toFileName(s string, style style) string { + switch style { + case kebabCase: + return toKebabCase(s) + case underscoreCase: + return toUnderscoreCase(s) + case camelCase: + return toCamelCase(s) + case pascalCase: + return toPascalCase(s) + case lowerCase: + return toPackageName(s) + default: + return toUnderscoreCase(s) + } +} + +func doWrite(file string, tpl string, replaces map[string]string) error { + s := os.Expand(tpl, func(s string) string { + switch { + case len(s) >= 3 && s[:3] == "Var": + return replaces[s] + case len(s) >= 6 && s[:6] == "Symbol": + return replaces[s] + default: + return "$" + s + } + }) + + if err := os.MkdirAll(filepath.Dir(file), os.ModePerm); err != nil { + return err + } + + return os.WriteFile(file, []byte(strings.TrimPrefix(s, "\n")), os.ModePerm) +} + +// 大写字母开头的字段才是导出的 +func isExportable(s string) bool { + r, _ := utf8.DecodeRuneInString(s) + return unicode.IsUpper(r) +}