package api import ( "encoding/json" "fmt" "net/url" "regexp" "strconv" "strings" "time" "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/api/errs" "github.com/davecgh/go-spew/spew" "github.com/eugeniucarvalho/validator" "github.com/kataras/iris/v12/context" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/bcrypt" ) var ( AcceptJson = regexp.MustCompile("^application/json") Environment = map[string]interface{}{} ToReference = references{} Format = fmt.Sprintf Dump = spew.Dump ) type references struct { } func (this *references) True() *bool { value := true return &value } func (this *references) False() *bool { value := false return &value } func (this *references) String(value string) *string { return &value } func (this *references) Bool(value bool) *bool { return &value } func (this *references) Int(value int) *int { return &value } func (this *references) Int32(value int32) *int32 { return &value } func (this *references) Int64(value int64) *int64 { return &value } func (this *references) Float64(value float64) *float64 { return &value } func (this *references) Float32(value float32) *float32 { return &value } type ModeInterface interface { Mode() string } type PatchHistoryRegister struct { Id primitive.ObjectID `bson:"_id" json:"-"` CreatedAt int64 `bson:"createdAt" json:"-"` CreatedBy string `bson:"createdBy" json:"-"` Parent string `bson:"parent" json:"-"` ApiTags []string `bson:"apiTags" json:"-"` } type EntityModel struct { // Id primitive.ObjectID `bson:"_id" json:"-"` Mode string `bson:"-" json:"-"` ApplicationVersion *string `bson:"appVersion,omitempty" json:"-"` Deleted *bool `bson:"deleted,omitempty" json:"-"` DeletedIn *int64 `bson:"deletedIn,omitempty" json:"-"` } // type DotNotation struct { // Rules map[string]interface{} `bson:",inline" json:"-"` // } // func (model *DotNotation) DotNotation() map[string]interface{} { // if model.Rules == nil { // model.Rules = map[string]interface{}{} // } // return model.Rules // } func PasswordBcryptHash(password string) (hash string, err *errs.Error) { var ( bytes []byte errgen error ) if bytes, errgen = bcrypt.GenerateFromPassword( []byte(password), 10, ); errgen != nil { err = errs.Internal().Details(&errs.Detail{ Reason: "passwordHashFailed", Message: "Falha ao processar a senha", }) return } hash = string(bytes) return } func UserIDString(ctx context.Context) (id string, err *errs.Error) { if user, ok := ctx.Values().Get("$user.ref").(map[string]interface{}); ok { id = user["id"].(primitive.ObjectID).Hex() return } err = errs.Internal().Details(&errs.Detail{ Message: "Invalid user instance", }) return } func (model *EntityModel) SetMode(mode string) { model.Mode = mode switch mode { case "create": model.ApplicationVersion = &BuildVersion deleted := false model.Deleted = &deleted case "update": if model.Deleted == nil { deleted := false model.Deleted = &deleted } case "patch": // if model.Deleted == nil { // deleted := false // model.Deleted = &deleted // } // case "delete": // case "undelete": } } func GetIDString(input interface{}) (string, *errs.Error) { if value, converted := input.(string); converted { return value, nil } if value, converted := input.(*primitive.ObjectID); converted { return value.Hex(), nil } if value, converted := input.(primitive.ObjectID); converted { return value.Hex(), nil } return "", errs.Internal().Details(&errs.Detail{ Reason: fmt.Sprintf("Can't convert ID into string. Invalid type '%T", input), }) } func NowUnix() int64 { return time.Now().Unix() } func GetUser(ctx context.Context) interface{} { return ctx.Values().Get("$user.ref") } func (model *EntityModel) SetDeleted(deleted bool) { var deletedIn = int64(0) model.Deleted = &deleted if *model.Deleted { deletedIn = time.Now().Unix() } model.DeletedIn = &deletedIn } type CorsOptions struct { ExposeHeaders []string AllowHeaders []string AllowMethods []string AllowOrigin []string } type GetManyResponse struct { ResultSizeEstimate int `json:"resultSizeEstimate"` // Estimativa do numero total de itens. NextPageToken string `json:"nextPageToken"` // Referência para a proxima pagina de resultados. Itens interface{} `json:"itens"` // Lista contento os elementos da resposta. } var ( // ParamsFlag map[string]*ParamFlag CorsDefaultOptions = CorsOptions{ AllowOrigin: []string{"*"}, // AllowOrigin: []string{"http://localhost:4200"}, AllowMethods: []string{"OPTIONS", "GET", "POST", "PUT", "DELETE", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin", "Host", "x-api-build"}, ExposeHeaders: []string{"X-total-count"}, } BuildVersion = "0" ApiVersion = "0" // Armazena os mimes dos arquivos consultados // MimeCache = xmap.NewMS2S() replaceEmpty = regexp.MustCompile(`\s+`) // pageTokenRegex = regexp.MustCompile(`(?P\w+):(?P\w+):(?P\d{1,6})`) pageTokenRegex = regexp.MustCompile(`(?P[\w-]+):(?P\d+)`) replaceIndex = regexp.MustCompile(`\.\d+\.`) ) func init() { // fmt.Println("Register validation functions") // err := Validator.RegisterValidation("req", func(f validator.FieldLevel) bool { // fmt.Println("Running req validation") // spew.Dump(f) // return true // }) validator.RegisterValidator("requiredOnCreate", func(i interface{}, o interface{}, v *validator.ValidatorOption) error { if schema, ok := o.(ModeInterface); ok { fmt.Println("requiredOnCreate ->", schema.Mode()) switch schema.Mode() { case "create": if i == nil { return fmt.Errorf("") } case "update": } } return nil }) // Validator.RegisterValidation("req", // func(fl validator.FieldLevel) bool { // // func(v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string) bool { // // func(v *validator.Validate, param string) bool { // // return passwordRegex.MatchString(field.String()) // }) // if err != nil { // panic(err) // } } func Panic() { if err := recover(); err != nil { LogError(0, err.(error).Error()) } } func Validate(i interface{}) *errs.Error { // if err := Validator.Struct(i); err != nil { // fmt.Println("VALIDATE", result, err) // spew.Dump(i) if errors, valid := validator.Struct(i); !valid { err := errs.InvalidArgument() // er := Error(ERR_INVALID_PARAM, "Invalid params") for _, e := range errors { err.Details(&errs.Detail{ Dominio: "global", Reason: "invalidParameter", Message: e.Message, // Message: fmt.Sprintf("%+v", e.Message), // LocationType:, // Location :, }) } // if _, ok := err.(*validator.InvalidValidationError); ok { // fmt.Println("INvalid") // er.LastDescription().Message = err.Error() // } else { // for _, err := range err.(validator.ValidationErrors) { // switch err.Tag() { // case "required": // } // er.Add(&ErrDescription{ // Dominio: "global", // Reason: "invalidParameter", // Message: fmt.Sprintf("%+v", err), // // LocationType:, // // Location :, // }) // // fmt.Println("1", err.Namespace()) // // fmt.Println("2", err.Field()) // // fmt.Println("3", err.StructNamespace()) // can differ when a custom TagNameFunc is registered or // // fmt.Println("4", err.StructField()) // by passing alt name to ReportError like below // // fmt.Println("5", err.Tag()) // // fmt.Println("6", err.ActualTag()) // // fmt.Println("7", err.Kind()) // // fmt.Println("8", err.Type()) // // fmt.Println("9", err.Value()) // // fmt.Println("10", err.Param()) // // fmt.Println("-------------") // } // } return err // from here you can create your own error messages in whatever language you wish } return nil } // ApiResponse é a estrutura padrão de respostas // Apenas os campos preenchidos serão retornados type ApiResponse struct { Entity interface{} `json:"entity,omitempty"` List interface{} `json:"list,omitempty"` NextPageToken string `json:"nextPageToken,omitempty"` ResultSizeEstimate int `json:"resultSizeEstimate,omitempty"` } // func ErroCtxHandler(ctx context.Context, err *errs.Error) { // if accept := ctx.GetHeader("Accept"); AcceptJson.Match([]byte(accept)) { // ctx.JSON(err) // } else { // ctx.ViewData("", err) // } // } func finalizeRequest(ctx context.Context, resp interface{}, err *errs.Error) { var ( status = 200 accept = ctx.Request().Header.Get("accept") types = strings.Split(accept, ",") ) defer func() { ctx.StopExecution() if debug := ctx.Values().Get("#debug"); debug != nil { go func() { debug.(*DebugTaks).Finalize() }() } if err != nil { ctx.Application().Logger().Error(err.Error()) if description := err.LastDescription(); description != nil { err.Stack.Print() // ctx.Application().Logger().Error(fmt.Printf("%s\n%s\n", description.Reason, description.Message)) } // ctx.Application().Logger().Error() spew.Dump(err) } fmt.Println("defer of finalizeRequest") if r := recover(); r != nil { fmt.Println("Recovered in f", r) } // fmt.Println(string(ctx.Values().Serialize())) }() if err != nil { status = err.HttpStatus // fmt.Println(status, err.Message, "------------------------------------------\n") // spew.Dump(err) // debbug // err.Stack.Print() resp = err abortTransaction(ctx) // fmt.Println("------------------------------------------\n", status) // spew.Dump(err) // fmt.Println("------------------------------------------\n", status) } ctx.Values().Set("res", resp) ctx.Header("x-api-build", BuildVersion) // fmt.Println("error") // spew.Dump(resp) // spew.Dump(types) // spew.Dump(ctx.Request().Header) ctx.StatusCode(status) for _, mime := range types { switch mime { case "application/json": ctx.JSON(resp) return } } // default response case ctx.WriteString("invalid accept header value: " + accept) } // Call encapsula e trata os erros para cada requisição. func CallAction(id string, fn func(context.Context) (interface{}, *errs.Error)) func(context.Context) { return func(ctx context.Context) { var ( err *errs.Error resp interface{} finalize = true values = ctx.Values() ) if debug, activeDebug := values.Get("#debug").(*DebugTaks); activeDebug { debug.Stage(id) } defer func() { if !ctx.IsStopped() { if _err := recover(); _err != nil { err = errs.Internal().Details(&errs.Detail{ Message: "", Location: fmt.Sprintf("call.action.%s", id), LocationType: "application.pipe.resource.stage", Reason: _err.(error).Error(), }) } if finalize { finalizeRequest(ctx, resp, err) } } }() fmt.Println("apply -> ", id) if resp, err = fn(ctx); err != nil { return } if !ctx.IsStopped() { if resp != nil { err = commitTransaction(ctx) } else { ctx.Next() finalize = false } } return } } func abortTransaction(ctx context.Context) (err *errs.Error) { return transactionHandler(ctx, "abort") } func commitTransaction(ctx context.Context) (err *errs.Error) { return transactionHandler(ctx, "commit") } func transactionHandler(ctx context.Context, action string) (err *errs.Error) { var ( localErr error operation func() error ) contextSession := GetSessionContext(ctx) if contextSession == nil { return } switch action { case "abort": operation = func() error { return contextSession.AbortTransaction(contextSession) } case "commit": operation = func() error { return contextSession.CommitTransaction(contextSession) } } defer func() { if localErr != nil { err = errs.Internal().Details(&errs.Detail{ Message: localErr.Error(), }) } }() try := 4 for { if localErr = operation(); localErr == nil { fmt.Println(action, "executed ") return } if try == 0 { return } try-- fmt.Println(action, "transaction error loop ") // time.Sleep(4 * time.Second) // retry operation when command contains TransientTransactionError // if cmdErr, ok := localErr.(mongo.CommandError); ok && cmdErr.HasErrorLabel("TransientTransactionError") { if cmdErr, ok := localErr.(mongo.CommandError); ok { fmt.Println(action, cmdErr) if cmdErr.HasErrorLabel("TransientTransactionError") { continue } } } } func ReadJson(ctx context.Context, entity interface{}) (err *errs.Error) { if err := ctx.ReadJSON(entity); err != nil { err = errs.DataCaps().Details(&errs.Detail{ Message: err.Error(), }) } return } func MgoSortBson(fields []string) *bson.M { order := bson.M{} for _, field := range fields { n := 1 if field != "" { fmt.Printf("sort '%c'\n", field[0]) switch field[0] { case '+': field = field[1:] case '-': n = -1 field = field[1:] default: panic(fmt.Sprintf("Invalid sort field %s.", field)) } } if field == "" { panic("Sort: empty field name") } field = string(replaceIndex.ReplaceAll([]byte(field), []byte("."))) order[field] = n } return &order } func MgoSort(ctx context.Context, field string) []string { result := []string{} if fields := Q(ctx, field, ""); fields != "" { sort := string(replaceEmpty.ReplaceAll([]byte(fields), []byte(""))) result = strings.Split(sort, ",") } // return nil return result } func MgoFieldsCtx(ctx context.Context, field string) *bson.M { return MgoFields(Q(ctx, field, "")) } func MgoFields(fields string) (projection *bson.M) { // fmt.Printf("MgoFields '%s'\n", fields) if fields != "" { projection = &bson.M{} for _, v := range strings.Split(fields, ",") { (*projection)[v] = 1 } // spew.Dump(projection) } return } func MgoQuery(ctx context.Context, field string) (*bson.M, *errs.Error) { return MgoQueryString(ctx, Q(ctx, field, "")) } func MgoQueryString(ctx context.Context, query string) (*bson.M, *errs.Error) { var ( selector = make(bson.M) // id = "_id" err error ) // Unmarshal json query if any if query != "" { if err = bson.UnmarshalExtJSON([]byte(query), true, &selector); err != nil { // return nil, Error(ERR_GENERAL, err.Error()) return nil, errs.Internal().Details(&errs.Detail{ Message: err.Error(), }) } if query, err = url.QueryUnescape(query); err != nil { return nil, errs.Internal().Details(&errs.Detail{ Message: err.Error(), }) // return nil, Error(ERR_GENERAL, err.Error()) } if err = json.Unmarshal([]byte(query), &selector); err != nil { // return nil, Error(ERR_GENERAL, err.Error()) return nil, errs.Internal().Details(&errs.Detail{ Message: err.Error(), }) } // if selector, err = mejson.Unmarshal(selector); err != nil { // return nil, Error(ERR_GENERAL, err.Error()) // } } // Transform string HexId to ObjectIdHex // if selid, _ := selector[id].(string); selid != "" { // if bson.IsObjectIdHex(selid) { // selector[id] = bson.ObjectIdHex(selid) // } // // else { // // selector[id] = selid // // } // } return &selector, nil } func DefaultCorsHandler() func(ctx context.Context) { return Cors(CorsDefaultOptions) } func Cors(opt CorsOptions) func(ctx context.Context) { return func(ctx context.Context) { ctx.Header("Access-Control-Allow-Credentials", "true") if len(opt.AllowOrigin) > 0 { // ctx.Header("Access-Control-Allow-Origin", strings.Join(opt.AllowOrigin, ",")) // ctx.Header("Access-Control-Allow-Origin", "*") // ctx.Header("Origin", "*") ctx.Header("Access-Control-Allow-Origin", ctx.GetHeader("Origin")) } if len(opt.AllowMethods) > 0 { ctx.Header("Access-Control-Allow-Methods", strings.Join(opt.AllowMethods, ",")) } if len(opt.AllowHeaders) > 0 { ctx.Header("Access-Control-Allow-Headers", strings.Join(opt.AllowHeaders, ",")) } if len(opt.ExposeHeaders) > 0 { ctx.Header("Access-Control-Expose-Headers", strings.Join(opt.ExposeHeaders, ",")) } ctx.Next() } } // Retorna um valor de um parametro no path da url. func P(ctx context.Context, name string, def string) string { val := ctx.Params().Get(name) if val == "" { val = def } return val } //Retorna um valor de um parametro da query. Ex: ?x=1 func Q(ctx context.Context, name string, def string) string { val := ctx.URLParam(name) if val == "" { val = def } return val } //Retorna um valor de um parametro da query func QInt(ctx context.Context, name string, def int) int { val, e := strconv.Atoi(Q(ctx, name, "")) if e != nil { val = def } return val } // Retorna um valor de um parametro do post. func F(ctx context.Context, name string, def interface{}) interface{} { var val interface{} val = ctx.FormValue(name) if val == "" { val = def } return val } func LogError(code int, m string) { log("31", fmt.Sprintf("[ERROR| %d] %s", code, m)) } func LogInfo(code int, m string) { log("34", fmt.Sprintf("[INFO| %d] %s", code, m)) } func LogWarning(code int, m string) { log("35", fmt.Sprintf("[WARNING| %d] %s", code, m)) } func log(color string, m string) { fmt.Printf("\x1b[%s;1m%s\x1b[0m\n", color, m) } func ObjectIDFromHex(objectId string) (id primitive.ObjectID, err *errs.Error) { var errd error if id, errd = primitive.ObjectIDFromHex(objectId); errd != nil { err = errs.InvalidArgument().Details(&errs.Detail{ Message: errd.Error(), Reason: "InvalidArgument", }) } return } // func ParseRequestTest(ctx context.Context) { // var ( // err error // filter = &Filter{ // MaxResults: QInt(ctx, "maxResults", 10), // } // oid primitive.ObjectID // ) // // parse parameter of path // id := P(ctx, "userId", "") // if oid, err = primitive.ObjectIDFromHex(id); err != nil { // filter.UserId = oid // } // id = P(ctx, "id", "") // if oid, err = primitive.ObjectIDFromHex(id); err != nil { // filter.Id = oid // } // // filter.PageToken.Parse(Q(ctx, "nextPageToken", "")) // if filter.Query, err = MgoQuery(ctx, "q"); err != nil { // goto Error // } // filter.Format = Q(ctx, "format", "full") // filter.Sort = MgoSortBson(MgoSort(ctx, "sort")) // filter.Fields = MgoFieldsCtx(ctx, "fields") // Error: // if err != nil { // ErroCtxHandler( // ctx, // // Error(ERR_INVALID_PARAM, err.Error()), // errs.Internal().Details(&errs.Detail{ // Message: err.Error(), // }), // ) // return // } // ctx.Values().Set("$filter", filter) // ctx.Next() // } // func MapError(erro *errs.Error) *errs.Error { // if strings.Contains(erro.Message, "E11000") { // return errs.AlreadyExists().Details(&errs.Detail{ // Message: "DUPLICATED_ITEM", // }) // } else if strings.Contains(erro.Message, "no documents in result") { // return errs.Internal().Details(&errs.Detail{ // Message: "NOT_FOUND", // }) // } // return erro // } // type PageToken struct { // // StartID string // // CurrentID string // Cursor string // NewCursor string // Page int // Count int // } // // Encode cria um novo token formatado // func (p *PageToken) Encode() string { // out := "" // if p.NewCursor != "" { // out = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d", p.NewCursor, p.Count))) // } // return out // // return string([]byte(fmt.Sprintf("%s:%d", p.NewCursor, p.Count))) // } // // HasToken determina se a requisição apresenta um token de paginacao // func (p *PageToken) HasToken() bool { // return p.Cursor != "" // } // // func (p *PageToken) GetBsonID() bson.ObjectId { // // if !bson.IsObjectIdHex(p.ID) { // // return nil // // } // // return bson.ObjectIdHex(p.ID) // // } // func (p *PageToken) Parse(s string) error { // var ( // decoded []byte // err error // ) // if decoded, err = base64.StdEncoding.DecodeString(s); err != nil { // return err // } // match := pageTokenRegex.FindStringSubmatch(string(decoded)) // if len(match) != 3 { // return fmt.Errorf("Invalid Page Token") // } // p.Cursor = match[1] // // p.Page, err = strconv.Atoi(match[2]) // p.Count, err = strconv.Atoi(match[2]) // return err // } // Layout aplica o path do layout // func Layout(ctx context.Context) { // ctx.ViewLayout(ViewScript(ctx, "layout/layout.html")) // ctx.Next() // } // // ViewScript devolve o path do arquivo de script a ser renderizaco // func ViewScript(ctx context.Context, filename string) string { // var ( // base string // ok bool // ) // domain := strings.Split(ctx.Request().Host, ":")[0] // if base, ok = TemplateDomainMap[domain]; !ok { // base = "default" // } // return base + "/" + filename // } // defer func() { // var ( // erro *errs.Error // // ok bool // err error // ) // if err = recover(); err != nil { // if erro, ok = err.(*errs.Error); !ok { // erro = Error(ERR_GENERAL, err.Error()) // } // ErroCtxHandler(ctx, erro) // } // // ctx.Header("Accept", "application/json") // // spew.Dump(err) // }() // func CallAction(f func(context.Context, *ApiResponse) *errs.Error) func(ctx context.Context) { // return func(ctx context.Context) { // var ( // err *errs.Error // response = &ApiResponse{} // ) // if err = f(ctx, response); err != nil { // ErroCtxHandler(ctx, err) // return // } // ctx.JSON(response) // } // } // Verifica se existe alguma pagina para ser carragada. // func UpdateCursorResponse(models *Mongo, f *Filter, resp interface{}) bool { // count := f.PageToken.Count // // Se não foi encontrado nenhum registro // if count > 0 { // resp.ResultSizeEstimate = count // resp.NextPageToken = f.PageToken.Encode() // return true // } // return false // }