[
  {
    "path": ".gitignore",
    "content": "/static/**\n.env\napp.db\n\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser"
  },
  {
    "path": "README.md",
    "content": "# GoGonicEcommerceApi\n# Table of Contents\n- [Introduction](#introduction)\n- [Full-stack Applications](#full-stack-applications)\n  * [E-commerce (shopping cart)](#e-commerce-shopping-cart)\n    + [Server side implementations](#server-side-implementations)\n    + [Client side implementations](#client-side-implementations)\n  * [Blog/CMS](#blogcms)\n    + [Server side implementations](#server-side-implementations-1)\n    + [Client side](#client-side)\n      - [The next come are](#the-next-come-are)\n  * [Simple CRUD(Create, Read, Update, Delete)](#simple-crudcreate-read-update-delete)\n    + [Server side implementations](#server-side-implementations-2)\n    + [Client side implementations](#client-side-implementations-1)\n      - [The next come are](#the-next-come-are-1)\n  * [CRUD + Pagination](#crud--pagination)\n    + [Server side implementations](#server-side-implementations-3)\n      - [The next come are](#the-next-come-are-2)\n    + [Client side implementations](#client-side-implementations-2)\n      - [The next come are](#the-next-come-are-3)\n- [Follow me](#social-media-links)\n    \n# Introduction\nThis is one of my E-commerce API app implementations. It is written in Golang using go-gonic web framework..\nThis is not a finished project by any means, but it has a valid enough shape to be git cloned and studied if you are interested in this topic.\nIf you are interested in this project take a look at my other server API implementations I have made with:\n\n# Full-stack Applications\n## E-commerce (shopping cart)\n### Server side implementations\n- [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SBootApiEcomMVCHibernate)\n- [Spring Boot + JAX-RS Jersey + Spring Data Hibernate](https://github.com/melardev/SpringBootEcommerceApiJersey)\n- [Node Js + Sequelize](https://github.com/melardev/ApiEcomSequelizeExpress)\n- [Node Js + Bookshelf](https://github.com/melardev/ApiEcomBookshelfExpress)\n- [Node Js + Mongoose](https://github.com/melardev/ApiEcomMongooseExpress)\n- [Python Django](https://github.com/melardev/DjangoRestShopApy)\n- [Flask](https://github.com/melardev/FlaskApiEcommerce)\n- [Golang go gonic](https://github.com/melardev/api_shop_gonic)\n- [Ruby on Rails](https://github.com/melardev/RailsApiEcommerce)\n- [AspNet Core](https://github.com/melardev/ApiAspCoreEcommerce)\n- [Laravel](https://github.com/melardev/ApiEcommerceLaravel)\n\nThe next to come are:\n- Spring Boot + Spring Data Hibernate + Kotlin\n- Spring Boot + Jax-RS Jersey + Hibernate + Kotlin\n- Spring Boot + mybatis\n- Spring Boot + mybatis + Kotlin\n- Asp.Net Web Api v2\n- Elixir\n- Golang + Beego\n- Golang + Iris\n- Golang + Echo\n- Golang + Mux\n- Golang + Revel\n- Golang + Kit\n- Flask + Flask-Restful\n- AspNetCore + NHibernate\n- AspNetCore + Dapper\n\n### Client side implementations\nThis client side E-commerce application is also implemented using other client side technologies:\n- [React Redux](https://github.com/melardev/ReactReduxEcommerceRestApi)\n- [React](https://github.com/melardev/ReactEcommerceRestApi)\n- [Vue](https://github.com/melardev/VueEcommerceRestApi)\n- [Vue + Vuex](https://github.com/melardev/VueVuexEcommerceRestApi)\n- [Angular](https://github.com/melardev/AngularEcommerceRestApi)\n\n## Blog/CMS\n### Server side implementations\n- [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiBlog)\n- [Go + Gin Gonic](https://github.com/melardev/GoGonicBlogApi)\n- [NodeJs + Mongoose](https://github.com/melardev/ApiBlogExpressMongoose)\n- [Laravel](https://github.com/melardev/LaravelApiBlog)\n- [Ruby on Rails + JBuilder](https://github.com/melardev/RailsApiBlog)\n- [Django + Rest-Framework](https://github.com/melardev/DjangoApiBlog)\n- [Asp.Net Core](https://github.com/melardev/AspCoreApiBlog)\n- [Flask + Flask-SQLAlchemy](https://github.com/melardev/FlaskApiBlog)\n\nThe next to come are:\n- Spring Boot + Spring Data Hibernate + Kotlin\n- Spring Boot + Jax-RS Jersey + Hibernate + Kotlin\n- Spring Boot + mybatis\n- Spring Boot + mybatis + Kotlin\n- Asp.Net Web Api v2\n- Elixir\n- Golang + Beego\n- Golang + Iris\n- Golang + Echo\n- Golang + Mux\n- Golang + Revel\n- Golang + Kit\n- Flask + Flask-Restful\n- AspNetCore + NHibernate\n- AspNetCore + Dapper\n\n### Client side\n- [Vue + Vuex](https://github.com/melardev/VueVuexBlog)\n- [Vue](https://github.com/melardev/VueBlog)\n- [React + Redux](https://github.com/melardev/ReactReduxBlog)\n- [React](https://github.com/melardev/ReactBlog)\n- [Angular](https://github.com/melardev/AngularBlog)\n\nThe next come are\n- Angular NgRx-Store\n- Angular + Material\n- React + Material\n- React + Redux + Material\n- Vue + Material\n- Vue + Vuex + Material\n- Ember\n\n## Simple CRUD(Create, Read, Update, Delete)\n### Server side implementations\n- [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiJpaCrud)\n- [Spring boot + Spring Data Reactive Mongo](https://github.com/melardev/SpringBootApiReactiveMongoCrud)\n- [Spring Boot + Spring Data Hibernate + Jersey](https://github.com/melardev/SpringBootApiJerseySpringDataCrud)\n- [NodeJs Express + Mongoose](https://github.com/melardev/ExpressMongooseApiCrud)\n- [Nodejs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiCrud)\n- [Nodejs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiCrud)\n- [Go + Gin-Gonic + Gorm](https://github.com/melardev/GoGinGonicApiGormCrud)\n- [Ruby On Rails](https://github.com/melardev/RailsApiCrud)\n- [Ruby On Rails + JBuilder](https://github.com/melardev/RailsApiJBuilderCrud)\n- [Laravel](https://github.com/melardev/LaravelApiCrud)\n- [AspNet Core](https://github.com/melardev/AspNetCoreApiCrud)\n- [AspNet Web Api 2](https://github.com/melardev/AspNetWebApiCrud)\n- [Python + Flask](https://github.com/melardev/FlaskApiCrud)\n- [Python + Django](https://github.com/melardev/DjanogApiCrud)\n- [Python + Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkCrud)\n\n### Client side implementations\n- [VueJs](https://github.com/melardev/VueAsyncCrud)\n\n#### The next come are\n- Angular NgRx-Store\n- Angular + Material\n- React + Material\n- React + Redux + Material\n- Vue + Material\n- Vue + Vuex + Material\n- Ember\n- Vanilla javascript\n\n## CRUD + Pagination\n### Server side implementations\n- [Spring Boot + Spring Data + Jersey](https://github.com/melardev/SpringBootJerseyApiPaginatedCrud)\n- [Spring Boot + Spring Data](https://github.com/melardev/SpringBootApiJpaPaginatedCrud)\n- [Spring Boot Reactive + Spring Data Reactive](https://github.com/melardev/ApiCrudReactiveMongo)\n- [Go with Gin Gonic](https://github.com/melardev/GoGinGonicApiPaginatedCrud)\n- [Laravel](https://github.com/melardev/LaravelApiPaginatedCrud)\n- [Rails + JBuilder](https://github.com/melardev/RailsJBuilderApiPaginatedCrud)\n- [Rails](https://github.com/melardev/RailsApiPaginatedCrud)\n- [NodeJs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiPaginatedCrud)\n- [NodeJs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiPaginatedCrud)\n- [NodeJs Express + Mongoose](https://github.com/melardev/ExpressApiMongoosePaginatedCrud)\n- [Python Django](https://github.com/melardev/DjangoApiCrudPaginated)\n- [Python Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkPaginatedCrud)\n- [Python Flask](https://github.com/melardev/FlaskApiPaginatedCrud)\n- [AspNet Core](https://github.com/melardev/AspNetCoreApiPaginatedCrud)\n- [AspNet Web Api 2](https://github.com/melardev/WebApiPaginatedAsyncCrud)\n\n#### The next come are\n- NodeJs Express + Knex\n- Flask + Flask-Restful\n- Laravel + Fractal\n- Laravel + ApiResources\n- Go with Mux\n- AspNet Web Api 2\n- Jersey\n- Elixir\n\n### Client side implementations\n- [Angular](https://github.com/melardev/AngularPaginatedAsyncCrud)\n- [React-Redux](https://github.com/melardev/ReactReduxPaginatedAsyncCrud)\n- [React](https://github.com/melardev/ReactAsyncPaginatedCrud)\n- [Vue + Vuex](https://github.com/melardev/VueVuexPaginatedAsyncCrud)\n- [Vue](https://github.com/melardev/VuePaginatedAsyncCrud)\n\n\n#### The next come are\n- Angular NgRx-Store\n- Angular + Material\n- React + Material\n- React + Redux + Material\n- Vue + Material\n- Vue + Vuex + Material\n- Ember\n- Vanilla javascript\n\n# Social media links\n- [Youtube Channel](https://youtube.com/melardev) I publish videos mainly on programming\n- [Blog](http://melardev.com) Sometimes I publish the source code there before Github\n- [Twitter](https://twitter.com/@melardev) I share tips on programming\n\n## WARNING\nI have mass of projects to deal with so I make some copy/paste around, if something I say is missing or is wrong, then I apologize\nand you may let me know opening an issue.\n\n# Getting started\n1. go get https://github.com/melardev/ApiEcomGoGonic\n1. Change the .env.example as you need(see warning below)\n1. Rename .env.example to .env\n1. Seed the database passing \"create seed\" as arguments to the app(read main.go to understand what I mean)\n\n## WARNING\nThe recommended database to use is Postgresql, the other database backends may not work as expected.\nUnfortunately the MySQL does not work as expected, for example the BeforeSave Hook for User is not able to retrieve\nthe Role model if using MySQL, the same code does work if SQLite, it is weird, because the SQL query generated is valid and it\nreturns a row, but somehow the driver is not able to map it to the user.\n\n# Features\n- Authentication / Authorization\n- JWT middleware for authentication\n- Multi file upload\n- Database seed\n- Paging with Limit and Offset using GORM (Golang ORM framework)\n- CRUD operations on products, comments, tags, categories, orders\n![Fetching products page](./github_images/postman.png)\n- Orders, guest users may place an order\n![Database diagram](./github_images/db_structure.png)\n\n# What you will learn\n- Golang\n- Golang Go-Gonic web framework\n- JWT\n- Controllers\n- Middlewares\n- JWT Authentication\n- Role based authorization\n- GORM\n    - associations: ManyToMany, OneToMany, ManyToOne\n    - virtual fields\n    - Select specific columns\n    - Eager loading\n    - Count related association\n    \n- seed data\n- misc\n    - project structure\n\n# Understanding the project\nThe project is meant to be educational, to learn something beyond the hello world thing we find in a lot, lot of \ntutorials and blog posts. Since its main goal is educational, I try to make as much use as features of APIs, in other\nwords, I used different code to do the same thing over and over, there is some repeated code but I tried to be as unique\nas possible so you can learn different ways of achieving the same goal.\n\nProject structure:\n- models: Mvc, it is our domain data.\n- dtos: it contains our serializers, they will create the response to be sent as json. They also take care of validating the input(feature incomplete)\n- controllers: well this is the mvC, they receive the request from the user, they ask the services to perform an action for them on the database.\n- seeds: contains the file that seeds the database.\n- static: a folder that will be generated when you create a product or tag or category with images\n- services: contains some business logic for each model, and for authorization\n- middlewares: it contains middlewares(golang functions) that are triggered before the controller action, for example, a middleware which\nreads the request looking for the Jwt token and trying to authenticate the user before forwarding the request to the corresponding controller\naction\n\n# TODO\n- Add model constraints such as not null\n- Refactor the seeding with http://gorm.io/docs/query.html#Select\n- Global Application Error handling\n- Can't Preload field errors:\n    - Get comment details http://127.0.0.1:8080/api/products/:slug/comments/:id triggered in services.FetchCommentById\n    - Get My Orders http://localhost:8080/api/orders triggered with services.FetchOrdersPage\n- Security, validations, file upload\n- Delete FileUpload if associated tag, category or product deleted\n- Delete Files if tag, category, product fail to be saved\n- Use pointers as function parameters instead of passing them by value as I did in many\n- For some reason /api/products does not work on browsers due to CORS issues, /api/home does work, on postman all \nroutes work ....\n# Resources\n- [Go-Gonic](https://github.com/gin-gonic/gin) Awesome golang based web framework\n- [GORM]()\n- [CORS gin's middleware](https://github.com/gin-contrib/cors)"
  },
  {
    "path": "controllers/addresses.go",
    "content": "package controllers\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\n\t\"net/http\"\n\t\"strconv\"\n)\n\nfunc RegisterAddressesRoutes(router *gin.RouterGroup) {\n\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.GET(\"/addresses\", ListAddresses)\n\t\trouter.POST(\"/addresses\", CreateAddress)\n\t}\n\n}\n\nfunc ListAddresses(c *gin.Context) {\n\n\tpageSizeStr := c.Query(\"page_size\")\n\tpageStr := c.Query(\"page\")\n\n\tpageSize, err := strconv.Atoi(pageSizeStr)\n\tif err != nil {\n\t\tpageSize = 5\n\t}\n\n\tpage, err := strconv.Atoi(pageStr)\n\tif err != nil {\n\t\tpage = 1\n\t}\n\n\t// userId:= c.Keys[\"currentUserId\"].(uint) // or\n\tuserId := c.MustGet(\"currentUserId\").(uint)\n\tincludeUser := false\n\taddresses, totalCommentCount := services.FetchAddressesPage(userId, page, pageSize, includeUser)\n\n\tc.JSON(http.StatusOK, dtos.CreateAddressPagedResponse(c.Request, addresses, page, pageSize, totalCommentCount, includeUser))\n}\n\nfunc CreateAddress(c *gin.Context) {\n\n\tuser := c.MustGet(\"currentUser\").(models.User)\n\n\tvar json dtos.CreateAddress\n\tif err := c.ShouldBindJSON(&json); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\tfirstName := json.FirstName\n\tlastName := json.LastName\n\tif firstName == \"\" {\n\t\tfirstName = user.FirstName\n\t}\n\tif lastName == \"\" {\n\t\tlastName = user.LastName\n\t}\n\taddress := models.Address{\n\t\tFirstName:     firstName,\n\t\tLastName:      lastName,\n\t\tCountry:       json.Country,\n\t\tCity:          json.City,\n\t\tStreetAddress: json.StreetAddress,\n\t\tZipCode:       json.ZipCode,\n\t\tUser:          user,\n\t\tUserId:        user.ID,\n\t}\n\n\tif err := services.SaveOne(&address); err != nil {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto(\"database_error\", err))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.GetAddressCreatedDto(&address, false))\n}\n"
  },
  {
    "path": "controllers/categories.go",
    "content": "package controllers\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc RegisterCategoryRoutes(router *gin.RouterGroup) {\n\trouter.GET(\"\", CategoryList)\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.POST(\"\", CreateCategory)\n\t}\n}\n\nfunc CategoryList(c *gin.Context) {\n\ttags, err := services.FetchAllCategories()\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"fetch_error\", err))\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, dtos.CreateCategoryListMapDto(tags))\n}\n\nfunc CreateCategory(c *gin.Context) {\n\tuser := c.MustGet(\"currentUser\").(models.User)\n\tif user.IsNotAdmin() {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"Permission denied, you must be admin\"))\n\t\treturn\n\t}\n\tname := c.PostForm(\"name\")\n\tdescription := c.PostForm(\"description\")\n\n\tform, err := c.MultipartForm()\n\tif err != nil {\n\t\tc.String(http.StatusBadRequest, fmt.Sprintf(\"get form err: %s\", err.Error()))\n\t\treturn\n\t}\n\tfiles := form.File[\"images[]\"]\n\tvar categoryImages = make([]models.FileUpload, len(files))\n\tfor index, file := range files {\n\t\tfileName := randomString(16) + \".png\"\n\n\t\tdirPath := filepath.Join(\".\", \"static\", \"images\", \"categories\")\n\t\tfilePath := filepath.Join(dirPath, fileName)\n\t\t// Create directory if does not exist\n\t\tif _, err = os.Stat(dirPath); os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(dirPath, os.ModeDir)\n\t\t\tif err != nil {\n\t\t\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"io_error\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Create file that will hold the image\n\t\toutputFile, err := os.Create(filePath)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tdefer outputFile.Close()\n\n\t\t// Open the temporary file that contains the uploaded image\n\t\tinputFile, err := file.Open()\n\t\tif err != nil {\n\t\t\tc.JSON(http.StatusOK, dtos.CreateDetailedErrorDto(\"io_error\", err))\n\t\t}\n\t\tdefer inputFile.Close()\n\n\t\t// Copy the temporary image to the permanent location outputFile\n\t\t_, err = io.Copy(outputFile, inputFile)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t\tc.String(http.StatusBadRequest, fmt.Sprintf(\"upload file err: %s\", err.Error()))\n\t\t\treturn\n\t\t}\n\n\t\tfileSize := (uint)(file.Size)\n\t\tcategoryImages[index] = models.FileUpload{Filename: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize}\n\t}\n\n\tdatabase := infrastructure.GetDb()\n\tcategory := models.Category{Name: name, Description: description, Images: categoryImages}\n\n\t// TODO: Why it is performing a SELECT SQL Query per image?\n\t// Even worse, it is selecting category_id, why??\n\t// SELECT \"tag_id\", \"product_id\" FROM \"file_uploads\"  WHERE (id = insertedFileUploadId)\n\terr = database.Create(&category).Error\n\tif err != nil {\n\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"db_error\", err))\n\t}\n\tc.JSON(http.StatusOK, dtos.CreateCategoryCreatedDto(category))\n}\n"
  },
  {
    "path": "controllers/comments.go",
    "content": "package controllers\n\nimport (\n\t\"errors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\n\t\"net/http\"\n\t\"strconv\"\n)\n\nfunc RegisterCommentRoutes(router *gin.RouterGroup) {\n\trouter.GET(\"/products/:slug/comments\", ListComments)\n\trouter.GET(\"/products/:slug/comments/:id\", ShowComment)\n\trouter.GET(\"/comments/:id\", ShowComment)\n\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.POST(\"/products/:slug/comments\", CreateComment)\n\t\trouter.DELETE(\"/comments/:id\", DeleteComment)\n\t\trouter.DELETE(\"/products/:slug/comments/:id\", DeleteComment)\n\t}\n\n}\n\nfunc ListComments(c *gin.Context) {\n\tslug := c.Param(\"slug\")\n\tdatabase := infrastructure.GetDb()\n\tproductId := -1\n\n\terr := database.Model(&models.Product{}).Where(&models.Product{Slug: slug}).Select(\"id\").Row().Scan(&productId)\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"comments\", errors.New(\"invalid slug\")))\n\t\treturn\n\t}\n\tpageSizeStr := c.Query(\"page_size\")\n\tpageStr := c.Query(\"page\")\n\n\tpageSize, err := strconv.Atoi(pageSizeStr)\n\tif err != nil {\n\t\tpageSize = 5\n\t}\n\n\tpage, err := strconv.Atoi(pageStr)\n\tif err != nil {\n\t\tpage = 1\n\t}\n\tcomments, totalCommentCount := services.FetchCommentsPage(productId, page, pageSize)\n\n\tc.JSON(http.StatusOK, dtos.CreateCommentPagedResponse(c.Request, comments, page, pageSize, totalCommentCount, true, false))\n}\n\nfunc CreateComment(c *gin.Context) {\n\tslug := c.Param(\"slug\")\n\tif slug == \"\" {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateErrorDtoWithMessage(\"You must provide a product slug you want to comment\"))\n\t\treturn\n\t}\n\n\tvar json dtos.CreateComment\n\tif err := c.ShouldBindJSON(&json); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tproductId, err := services.FetchProductId(slug)\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"database_error\", err))\n\t\treturn\n\t}\n\n\tcomment := models.Comment{\n\t\tContent:   json.Content,\n\t\tProductId: productId,\n\t\tUser:      c.MustGet(\"currentUser\").(models.User),\n\t\tUserId:    c.MustGet(\"currentUserId\").(uint),\n\t}\n\n\tif err := services.SaveOne(&comment); err != nil {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto(\"database_error\", err))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreateCommentCreatedDto(&comment))\n}\n\nfunc ShowComment(c *gin.Context) {\n\tidStr := c.Param(\"id\")\n\tid, err := strconv.Atoi(idStr)\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateErrorDtoWithMessage(\"You must provide a valid comment id\"))\n\t}\n\tcomment := services.FetchCommentById(id, true, true)\n\tc.JSON(http.StatusOK, dtos.GetCommentDetailsDto(&comment, true, true))\n}\n\nfunc DeleteComment(c *gin.Context) {\n\tcurrentUser := c.MustGet(\"currentUser\").(models.User)\n\n\tid64, err := strconv.ParseUint(c.Param(\"id\"), 10, 32)\n\tid := uint(id64)\n\tdatabase := infrastructure.GetDb()\n\tvar comment models.Comment\n\terr = database.Select([]string{\"id\", \"user_id\"}).Find(&comment, id).Error\n\tif err != nil || comment.ID == 0 {\n\t\t// the comment.ID == is redundat, but shows the other way of checking but it is less readable\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"comment\", err))\n\t} else if currentUser.ID == comment.UserId || currentUser.IsAdmin() {\n\t\terr = database.Delete(&comment).Error\n\t\tif err != nil {\n\t\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"database_error\", err))\n\t\t\treturn\n\t\t}\n\t\tc.JSON(http.StatusOK, dtos.CreateSuccessWithMessageDto(\"Comment Deleted successfully\"))\n\t} else {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"You have to be admin or the owner of this comment to delete it\"))\n\t}\n}\n"
  },
  {
    "path": "controllers/orders.go",
    "content": "package controllers\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\t\"net/http\"\n\t\"strconv\"\n)\n\nfunc RegisterOrderRoutes(router *gin.RouterGroup) {\n\trouter.POST(\"\", CreateOrder)\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.GET(\"\", ListOrders)\n\t\trouter.GET(\"/:id\", ShowOrder)\n\t}\n}\n\nfunc ListOrders(c *gin.Context) {\n\tpageSizeStr := c.Query(\"page_size\")\n\tpageStr := c.Query(\"page\")\n\tpageSize, err := strconv.Atoi(pageSizeStr)\n\tif err != nil {\n\t\tpageSize = 5\n\t}\n\n\tpage, err := strconv.Atoi(pageStr)\n\tif err != nil {\n\t\tpage = 1\n\t}\n\tuserId := c.MustGet(\"currentUserId\").(uint)\n\n\torders, totalCommentCount, err := services.FetchOrdersPage(userId, page, pageSize)\n\n\tc.JSON(http.StatusOK, dtos.CreateOrderPagedResponse(c.Request, orders, page, pageSize, totalCommentCount, false, false))\n}\n\nfunc ShowOrder(c *gin.Context) {\n\torderId, err := strconv.Atoi(c.Param(\"id\"))\n\tuser := c.MustGet(\"currentUser\").(models.User)\n\torder, err := services.FetchOrderDetails(uint(orderId))\n\tif err != nil {\n\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"db_error\", err))\n\t\treturn\n\t}\n\n\tif order.UserId == user.ID || user.IsAdmin() {\n\t\tc.JSON(http.StatusOK, dtos.CreateOrderDetailsDto(&order))\n\t} else {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"Permission denied, you can not view this order\"))\n\t\treturn\n\t}\n}\n\nfunc CreateOrder(c *gin.Context) {\n\tvar orderRequest dtos.CreateOrderRequestDto\n\tif err := c.ShouldBind(&orderRequest); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tuserObj, userLoggedIn := c.Get(\"currentUser\")\n\tvar user models.User\n\tif userLoggedIn {\n\t\tuser = (userObj).(models.User)\n\t}\n\n\tvar address models.Address\n\t// Reuse address can only be done by authenticated users\n\tif orderRequest.AddressId != 0 && userLoggedIn {\n\t\taddress = services.FetchAddress(orderRequest.AddressId)\n\t\t/*if err != nil || address.ID == 0 {\n\t\t\tc.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto(\"db_error\", err))\n\t\t\treturn\n\t\t}*/\n\t\tif address.UserId != user.ID {\n\t\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"permission denied\"))\n\t\t\treturn\n\t\t}\n\t} else if orderRequest.AddressId == 0 {\n\t\taddress = models.Address{\n\t\t\tFirstName:     orderRequest.FirstName,\n\t\t\tLastName:      orderRequest.LastName,\n\t\t\tCity:          orderRequest.City,\n\t\t\tCountry:       orderRequest.Country,\n\t\t\tStreetAddress: orderRequest.StreetAddress,\n\t\t\tZipCode:       orderRequest.ZipCode,\n\t\t}\n\t\tif userLoggedIn {\n\t\t\taddress.UserId = user.ID\n\t\t}\n\t\terr := services.CreateOne(&address)\n\t\tif err != nil {\n\t\t\tc.JSON(http.StatusInternalServerError, err)\n\t\t\treturn\n\t\t}\n\n\t} else {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"Operation not supported, what are you trying to do?\"))\n\t\treturn\n\t}\n\n\torder := models.Order{\n\t\tTrackingNumber: randomString(16),\n\t\tOrderStatus:    0,\n\t\tAddress:        address,\n\t\tAddressId:      address.ID,\n\t}\n\n\tif userLoggedIn {\n\t\torder.UserId = user.ID\n\t\torder.User = user\n\t}\n\n\tvar productIds = make([]uint, len(orderRequest.CartItems))\n\tfor i := 0; i < len(orderRequest.CartItems); i++ {\n\t\tproductIds[i] = orderRequest.CartItems[i].Id\n\t}\n\n\tproducts, err := services.FetchProductsIdNameAndPrice(productIds)\n\tif err != nil {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto(\"db_error\", err))\n\t\treturn\n\t}\n\n\tif len(products) != len(orderRequest.CartItems) {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateErrorDtoWithMessage(\"make sure all products are still available\"))\n\t\treturn\n\t}\n\torderItems := make([]models.OrderItem, len(products))\n\n\tfor i := 0; i < len(products); i++ {\n\t\t// I am assuming product ids returned are in the same order as the cart_items, TODO: implement a more robust code to ensure\n\t\torderItems[i] = models.OrderItem{\n\t\t\tProductId:   products[i].ID,\n\t\t\tProductName: products[i].Name,\n\t\t\tSlug:        products[i].Slug,\n\t\t\tQuantity:    orderRequest.CartItems[i].Quantity,\n\t\t}\n\t}\n\n\torder.OrderItems = orderItems\n\terr = services.CreateOne(&order)\n\tif err != nil {\n\t\tc.JSON(http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, dtos.CreateOrderCreatedDto(&order))\n\n}\n"
  },
  {
    "path": "controllers/pages.go",
    "content": "package controllers\n\nimport (\n\t\"errors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\t\"net/http\"\n)\n\nfunc RegisterPageRoutes(router *gin.RouterGroup) {\n\trouter.GET(\"\", Home)\n\trouter.GET(\"/home\", Home)\n\n}\n\nfunc Home(c *gin.Context) {\n\n\ttags, err := services.FetchAllTags()\n\tcategories, err := services.FetchAllCategories()\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"comments\", errors.New(\"Somethign went wrong\")))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreateHomeResponse(tags, categories))\n}\n"
  },
  {
    "path": "controllers/products.go",
    "content": "package controllers\n\n// import \"C\"\nimport (\n\t\"errors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gosimple/slug\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\n\t\"net/http\"\n\t\"strconv\"\n)\n\nfunc RegisterProductRoutes(router *gin.RouterGroup) {\n\trouter.GET(\"/\", ProductList)\n\trouter.GET(\"/:slug\", GetProductDetailsBySlug)\n\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.POST(\"/\", CreateProduct)\n\t\trouter.DELETE(\"/:slug\", ProductDelete)\n\t}\n}\n\nfunc ProductList(c *gin.Context) {\n\n\tpageSizeStr := c.Query(\"page_size\")\n\tpageStr := c.Query(\"page\")\n\n\tpageSize, err := strconv.Atoi(pageSizeStr)\n\tif err != nil {\n\t\tpageSize = 5\n\t}\n\n\tpage, err := strconv.Atoi(pageStr)\n\tif err != nil {\n\t\tpage = 1\n\t}\n\n\tproductModels, modelCount, commentsCount, err := services.FetchProductsPage(page, pageSize)\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"products\", errors.New(\"Invalid param\")))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreatedProductPagedResponse(c.Request, productModels, page, pageSize, modelCount, commentsCount))\n}\n\nfunc GetProductDetailsBySlug(c *gin.Context) {\n\tproductSlug := c.Param(\"slug\")\n\n\tproduct := services.FetchProductDetails(&models.Product{Slug: productSlug}, true)\n\tif product.ID == 0 {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"products\", errors.New(\"Invalid slug\")))\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, dtos.CreateProductDetailsDto(product))\n}\n\nfunc CreateProduct(c *gin.Context) {\n\t// Only admin users can create products\n\tuser := c.Keys[\"currentUser\"].(models.User)\n\tif user.IsNotAdmin() {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"Permission denied, you must be admin\"))\n\t\treturn\n\t}\n\n\tvar formDto dtos.CreateProduct\n\tif err := c.ShouldBind(&formDto); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tname := formDto.Name\n\tdescription := formDto.Description\n\n\tprice := formDto.Price\n\tstock, err := strconv.ParseInt(c.PostForm(\"stock\"), 10, 32)\n\tform, err := c.MultipartForm()\n\n\ttagCount := 0\n\tcatCount := 0\n\tfor key := range form.Value {\n\t\tif strings.HasPrefix(key, \"tags[\") {\n\t\t\ttagCount++\n\t\t}\n\t\tif strings.HasPrefix(key, \"category[\") {\n\t\t\tcatCount++\n\t\t}\n\t}\n\n\tvar tags = make([]models.Tag, tagCount)\n\tvar categories = make([]models.Category, catCount)\n\n\tvar rgx = regexp.MustCompile(`\\[(.*?)\\]`)\n\tdatabase := infrastructure.GetDb()\n\ttagPtr := 0\n\tcatPtr := 0\n\n\tfor k, v := range form.Value {\n\t\tif strings.HasPrefix(k, \"tags[\") {\n\t\t\tresult := rgx.FindStringSubmatch(k)\n\t\t\tvar tag models.Tag\n\t\t\tname := result[1]\n\t\t\tdescription := v[0]\n\t\t\tdatabase.Where(&models.Tag{Slug: slug.Make(name)}).\n\t\t\t\tAttrs(models.Tag{Name: name, Description: description}).\n\t\t\t\tFirstOrCreate(&tag)\n\t\t\ttags[tagPtr] = tag\n\t\t\ttagPtr++\n\t\t}\n\n\t\tif strings.HasPrefix(k, \"category[\") {\n\t\t\tresult := rgx.FindStringSubmatch(k)\n\t\t\tvar category models.Category\n\t\t\tname := result[1]\n\t\t\tdescription := v[0]\n\t\t\tdatabase.Where(&models.Category{Slug: slug.Make(name)}).\n\t\t\t\tAttrs(models.Category{Name: name, Description: description}).\n\t\t\t\tFirstOrCreate(&category)\n\t\t\tcategories[catPtr] = category\n\t\t\tcatPtr++\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto(\"form_error\", err))\n\t\treturn\n\t}\n\n\tfiles := form.File[\"images[]\"]\n\tvar productImages = make([]models.FileUpload, len(files))\n\n\tfor index, file := range files {\n\t\tfileName := randomString(16) + \".png\"\n\n\t\tdirPath := filepath.Join(\".\", \"static\", \"images\", \"products\")\n\t\tfilePath := filepath.Join(dirPath, fileName)\n\t\tif _, err = os.Stat(dirPath); os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(dirPath, os.ModeDir)\n\t\t\tif err != nil {\n\t\t\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"io_error\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif err := c.SaveUploadedFile(file, filePath); err != nil {\n\t\t\tc.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto(\"upload_error\", err))\n\t\t\treturn\n\t\t}\n\t\tfileSize := (uint)(file.Size)\n\t\tproductImages[index] = models.FileUpload{Filename: fileName, OriginalName: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize}\n\t}\n\n\tproduct := models.Product{\n\t\tName:        name,\n\t\tDescription: description,\n\t\tTags:        tags,\n\t\tCategories:  categories,\n\t\tPrice:       (int)(price),\n\t\tStock:       (int)(stock),\n\t\tImages:      productImages,\n\t}\n\n\tif err := services.CreateOne(&product); err != nil {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto(\"database\", err))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreateProductCreatedDto(product))\n\n}\n\nfunc ProductDelete(c *gin.Context) {\n\tslug := c.Param(\"slug\")\n\terr := services.DeleteProduct(&models.Product{Slug: slug})\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"products\", errors.New(\"Invalid slug\")))\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, gin.H{\"product\": \"Delete success\"})\n}\n"
  },
  {
    "path": "controllers/tags.go",
    "content": "package controllers\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc RegisterTagRoutes(router *gin.RouterGroup) {\n\trouter.GET(\"\", TagList)\n\trouter.Use(middlewares.EnforceAuthenticatedMiddleware())\n\t{\n\t\trouter.POST(\"\", CreateTag)\n\t}\n}\n\nfunc TagList(c *gin.Context) {\n\ttags, err := services.FetchAllTags()\n\tif err != nil {\n\t\tc.JSON(http.StatusNotFound, dtos.CreateDetailedErrorDto(\"fetch_error\", err))\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, dtos.CreateTagListMapDto(tags))\n}\n\nvar letterRunes = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\nfunc randomString(length int) string {\n\tb := make([]rune, length)\n\tfor i := range b {\n\t\tb[i] = letterRunes[rand.Intn(len(letterRunes))]\n\t}\n\treturn string(b)\n}\n\nfunc CreateTag(c *gin.Context) {\n\tuser := c.Keys[\"currentUser\"].(models.User)\n\tif user.IsNotAdmin() {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateErrorDtoWithMessage(\"Permission denied, you must be admin\"))\n\t\treturn\n\t}\n\tvar createForm dtos.CreateTag\n\t// name := c.PostForm(\"name\")\n\t// description := c.PostForm(\"description\")\n\n\t// If you wanna know more about how binding is done internally check gin-gonic/bin/binding.formBinding.Bind at form.go\n\tif err := c.ShouldBind(&createForm); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tform, err := c.MultipartForm()\n\tif err != nil {\n\t\tc.String(http.StatusBadRequest, fmt.Sprintf(\"get form err: %s\", err.Error()))\n\t\treturn\n\t}\n\n\tfiles := form.File[\"images[]\"]\n\tvar tagImages = make([]models.FileUpload, len(files))\n\tfor index, file := range files {\n\t\tfileName := randomString(16) + \".png\"\n\n\t\tdirPath := filepath.Join(\".\", \"static\", \"images\", \"tags\")\n\t\tfilePath := filepath.Join(dirPath, fileName)\n\t\tif _, err = os.Stat(dirPath); os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(dirPath, os.ModeDir)\n\t\t\tif err != nil {\n\t\t\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"io_error\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif err := c.SaveUploadedFile(file, filePath); err != nil {\n\t\t\tc.JSON(http.StatusBadRequest, dtos.CreateDetailedErrorDto(\"upload_error\", err))\n\t\t\treturn\n\t\t}\n\t\tfileSize := (uint)(file.Size)\n\t\ttagImages[index] = models.FileUpload{Filename: fileName, OriginalName: file.Filename, FilePath: string(filepath.Separator) + filePath, FileSize: fileSize}\n\t}\n\n\tdatabase := infrastructure.GetDb()\n\ttag := models.Tag{Name: createForm.Name, Description: createForm.Description, Images: tagImages}\n\t// TODO: Why it is performing a SELECT SQL Query per image?\n\t// Even worse, it is selecting category_id, why??\n\t// SELECT \"category_id\", \"product_id\" FROM \"file_uploads\"  WHERE (id = insertedFileUploadId)\n\terr = database.Create(&tag).Error\n\tif err != nil {\n\t\tc.JSON(http.StatusInternalServerError, dtos.CreateDetailedErrorDto(\"db_error\", err))\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreateTagCreatedDto(tag))\n}\n"
  },
  {
    "path": "controllers/users.go",
    "content": "package controllers\n\nimport (\n\t\"errors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/dtos\"\n\t\"github.com/melardev/GoGonicEcommerceApi/services\"\n\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"net/http\"\n)\n\nfunc RegisterUserRoutes(router *gin.RouterGroup) {\n\trouter.POST(\"/\", UsersRegistration)\n\trouter.POST(\"/login\", UsersLogin)\n}\n\nfunc UsersRegistration(c *gin.Context) {\n\n\tvar json dtos.RegisterRequestDto\n\tif err := c.ShouldBindJSON(&json); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tpassword, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)\n\tif err := services.CreateOne(&models.User{\n\t\tUsername:  json.Username,\n\t\tPassword:  string(password),\n\t\tFirstName: json.FirstName,\n\t\tLastName:  json.LastName,\n\t\tEmail:     json.Email,\n\t}); err != nil {\n\t\tc.JSON(http.StatusUnprocessableEntity, dtos.CreateDetailedErrorDto(\"database\", err))\n\t\treturn\n\t}\n\tc.JSON(http.StatusCreated, gin.H{\n\t\t\"success\":       true,\n\t\t\"full_messages\": []string{\"User created successfully\"}})\n}\n\nfunc UsersLogin(c *gin.Context) {\n\n\tvar json dtos.LoginRequestDto\n\tif err := c.ShouldBindJSON(&json); err != nil {\n\t\tc.JSON(http.StatusBadRequest, dtos.CreateBadRequestErrorDto(err))\n\t\treturn\n\t}\n\n\tuser, err := services.FindOneUser(&models.User{Username: json.Username})\n\n\tif err != nil {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateDetailedErrorDto(\"login_error\", err))\n\t\treturn\n\t}\n\n\tif user.IsValidPassword(json.Password) != nil {\n\t\tc.JSON(http.StatusForbidden, dtos.CreateDetailedErrorDto(\"login\", errors.New(\"invalid credentials\")))\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, dtos.CreateLoginSuccessful(&user))\n\n}\n"
  },
  {
    "path": "dtos/addresses.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"net/http\"\n)\n\ntype CreateAddress struct {\n\tFirstName     string `form:\"first_name\" json:\"first_name\" xml:\"first_name\"`\n\tLastName      string `form:\"last_name\" json:\"last_name\" xml:\"last_name\"`\n\tCountry       string `form:\"country\" json:\"country\" xml:\"country\" binding:\"required\"`\n\tCity          string `form:\"city\" json:\"city\" xml:\"city\" binding:\"required\"`\n\tStreetAddress string `form:\"address\" json:\"address\" xml:\"address\" binding:\"required\"`\n\tZipCode       string `form:\"zip_code\" json:\"zip_code\" xml:\"zip_code\" binding:\"required\"`\n}\n\nfunc CreateAddressPagedResponse(request *http.Request, addresses []models.Address, page, page_size, count int, includeUser bool) map[string]interface{} {\n\tvar resources = make([]interface{}, len(addresses))\n\tfor index, address := range addresses {\n\t\tresources[index] = GetAddressDto(&address, includeUser)\n\t}\n\treturn CreatePagedResponse(request, resources, \"addresses\", page, page_size, count)\n}\n\nfunc GetAddressDto(address *models.Address, includeUser bool) map[string]interface{} {\n\tdto := map[string]interface{}{\n\t\t\"id\":         address.ID,\n\t\t\"first_name\": address.FirstName,\n\t\t\"last_name\":  address.LastName,\n\t\t\"zip_code\":   address.ZipCode,\n\t\t\"country\":    address.Country,\n\t\t\"city\":       address.City,\n\t}\n\n\tif includeUser {\n\t\tdto[\"user\"] = map[string]interface{}{\n\t\t\t\"id\":       address.UserId,\n\t\t\t\"username\": address.User.Username,\n\t\t}\n\t}\n\treturn dto\n}\n\nfunc GetAddressCreatedDto(address *models.Address, includeUser bool) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(GetAddressDto(address, includeUser), \"StreetAddress created successfully\")\n}\n"
  },
  {
    "path": "dtos/categories.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"strings\"\n)\n\nfunc CreateCategoryListMapDto(categories []models.Category) map[string]interface{} {\n\tresult := map[string]interface{}{}\n\tvar t = make([]interface{}, len(categories))\n\tfor i := 0; i < len(categories); i++ {\n\t\tt[i] = CreateCategoryDto(categories[i])\n\t}\n\tresult[\"categories\"] = t\n\treturn CreateSuccessDto(result)\n}\n\nfunc CreateCategoryListDto(categories []models.Category) []interface{} {\n\tvar t = make([]interface{}, len(categories))\n\tfor i := 0; i < len(categories); i++ {\n\t\tt[i] = CreateCategoryDto(categories[i])\n\t}\n\treturn t\n}\n\nfunc CreateCategoryDto(category models.Category) map[string]interface{} {\n\tvar imageUrls = make([]string, len(category.Images))\n\treplaceAllFlag := -1\n\tfor i := 0; i < len(category.Images); i++ {\n\t\timageUrls[i] = strings.Replace(category.Images[i].FilePath, \"\\\\\", \"/\", replaceAllFlag)\n\t}\n\treturn map[string]interface{}{\n\t\t\"id\":          category.ID,\n\t\t\"name\":        category.Name,\n\t\t\"description\": category.Description,\n\t\t\"image_urls\":  imageUrls,\n\t}\n}\n\nfunc CreateCategoryCreatedDto(category models.Category) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(CreateCategoryDto(category), \"Category created successfully\")\n}\n"
  },
  {
    "path": "dtos/comments.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype CreateComment struct {\n\tContent string `form:\"content\" json:\"content\" xml:\"content\"  binding:\"required\"`\n}\n\nfunc CreateCommentPagedResponse(request *http.Request, comments []models.Comment, page, page_size, count int, bools ...bool) map[string]interface{} {\n\tvar resources = make([]interface{}, len(comments))\n\tfor index, comment := range comments {\n\t\tincludeUser := false\n\t\tif len(bools) > 0 {\n\t\t\tincludeUser = bools[0]\n\t\t}\n\t\tincludeProduct := false\n\t\tif len(bools) > 1 {\n\t\t\tincludeProduct = bools[1]\n\t\t}\n\n\t\tresources[index] = GetSummary(&comment, includeUser, includeProduct)\n\t}\n\treturn CreatePagedResponse(request, resources, \"comments\", page, page_size, count)\n}\n\nfunc GetCommentDetailsDto(comment *models.Comment, includes ...bool) map[string]interface{} {\n\tincludeUser := false\n\tif len(includes) > 0 {\n\t\tincludeUser = includes[0]\n\t}\n\tincludeProduct := false\n\tif len(includes) > 1 {\n\t\tincludeProduct = includes[1]\n\t}\n\treturn GetSummary(comment, includeUser, includeProduct)\n}\n\nfunc GetSummary(comment *models.Comment, includeUser, includeProduct bool) map[string]interface{} {\n\tresult := map[string]interface{}{\n\t\t\"id\":         comment.ID,\n\t\t\"content\":    comment.Content,\n\t\t\"created_at\": comment.CreatedAt.UTC().Format(time.RFC1123),\n\t\t\"updated_at\": comment.UpdatedAt.UTC().Format(time.RFC1123),\n\t}\n\tif includeUser == true {\n\t\tresult[\"user\"] = map[string]interface{}{\n\t\t\t\"id\":       comment.User.ID,\n\t\t\t\"username\": comment.User.Username,\n\t\t}\n\t}\n\tif includeProduct == true {\n\t\tresult[\"product\"] = map[string]interface{}{\n\t\t\t\"id\":   comment.Product.ID,\n\t\t\t\"name\": comment.Product.Name,\n\t\t\t\"slug\": comment.Product.Slug,\n\t\t}\n\t}\n\treturn result\n}\n\nfunc CreateCommentCreatedDto(comment *models.Comment, includes ...bool) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(GetCommentDetailsDto(comment, includes...), \"Comment created successfully\")\n}\n"
  },
  {
    "path": "dtos/orders.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"net/http\"\n)\n\ntype CreateOrderRequestDto struct {\n\tFirstName     string `form:\"first_name\" json:\"first_name\" xml:\"first_name\"`\n\tLastName      string `form:\"last_name\" json:\"last_name\" xml:\"last_name\"`\n\tCountry       string `form:\"country\" json:\"country\" xml:\"country\"`\n\tCity          string `form:\"city\" json:\"city\" xml:\"city\"`\n\tStreetAddress string `form:\"street_address\" json:\"street_address\" xml:\"street_address\" `\n\tZipCode       string `form:\"zip_code\" json:\"zip_code\" xml:\"zip_code\" `\n\tAddressId     uint   `form:\"address_id\" json:\"address_id\" xml:\"address_id\" `\n\tCartItems     []struct {\n\t\tId       uint `form:\"id\" json:\"id\" binding:\"required\"`\n\t\tQuantity int  `form:\"quantity\" json:\"quantity\" binding:\"required\"`\n\t} `json:\"cart_items\"`\n}\n\nfunc CreateOrderPagedResponse(request *http.Request, orders []models.Order, page, page_size, totalOrdersCount int, includes ...bool) map[string]interface{} {\n\tvar resources = make([]interface{}, len(orders))\n\tfor index, order := range orders {\n\n\t\tincludeAddress, includeOrderItems, includeUser := getIncludeFlags(includes...)\n\n\t\tresources[index] = CreateOrderDto(&order, includeAddress, includeOrderItems, includeUser)\n\t}\n\treturn CreatePagedResponse(request, resources, \"orders\", page, page_size, totalOrdersCount)\n}\n\nfunc CreateOrderDto(order *models.Order, includes ...bool) map[string]interface{} {\n\n\tincludeAddress, includeOrderItems, includeUser := getIncludeFlags(includes...)\n\n\tresult := map[string]interface{}{\n\t\t\"id\":              order.ID,\n\t\t\"tracking_number\": order.TrackingNumber,\n\t\t\"order_status\":    order.GetOrderStatusAsString(),\n\t}\n\n\tif includeAddress {\n\t\tresult[\"address\"] = map[string]interface{}{\n\t\t\t\"first_name\":     order.Address.FirstName,\n\t\t\t\"last_name\":      order.Address.LastName,\n\t\t\t\"street_address\": order.Address.StreetAddress,\n\t\t\t\"city\":           order.Address.City,\n\t\t\t\"country\":        order.Address.Country,\n\t\t\t\"zip_code\":       order.Address.ZipCode,\n\t\t}\n\t}\n\n\tif includeOrderItems {\n\t\torderItems := make([]map[string]interface{}, len(order.OrderItems))\n\t\tfor i := 0; i < len(order.OrderItems); i++ {\n\t\t\toi := order.OrderItems[i]\n\t\t\torderItems[i] = map[string]interface{}{\n\t\t\t\t\"name\":  oi.ProductName,\n\t\t\t\t\"slug\":  oi.Slug,\n\t\t\t\t\"price\": oi.Price,\n\t\t\t}\n\t\t}\n\t\tresult[\"order_items\"] = orderItems\n\t} else {\n\t\tresult[\"order_items_count\"] = order.OrderItemsCount\n\t}\n\n\tif includeUser {\n\t\tresult[\"user\"] = map[string]interface{}{\n\t\t\t\"id\":       order.UserId,\n\t\t\t\"username\": order.User.Username,\n\t\t}\n\t}\n\n\treturn CreateSuccessDto(result)\n}\n\nfunc CreateOrderDetailsDto(order *models.Order) map[string]interface{} {\n\t// includeUser -> false\n\t// includeOrderItems -> true\n\t// includeUser -> false\n\treturn CreateSuccessDto(CreateOrderDto(order, true, true, false))\n}\n\nfunc getIncludeFlags(includes ...bool) (includeAddress, includeOrderItems, includeUser bool) {\n\n\tif len(includes) > 0 {\n\t\tincludeAddress = includes[0]\n\t}\n\n\tif len(includes) > 1 {\n\t\tincludeOrderItems = includes[1]\n\t}\n\n\tif len(includes) > 2 {\n\t\tincludeUser = includes[2]\n\t}\n\treturn\n}\n\nfunc CreateOrderCreatedDto(order *models.Order) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(CreateOrderDetailsDto(order), \"Order created successfully\")\n}\n"
  },
  {
    "path": "dtos/pages.go",
    "content": "package dtos\n\nimport \"github.com/melardev/GoGonicEcommerceApi/models\"\n\nfunc CreateHomeResponse(tags []models.Tag, categories []models.Category) map[string]interface{} {\n\treturn CreateSuccessDto(map[string]interface{}{\n\t\t\"tags\":       CreateTagListDto(tags),\n\t\t\"categories\": CreateCategoryListDto(categories),\n\t})\n}\n"
  },
  {
    "path": "dtos/products.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype ManagedModel models.Product\n\ntype CreateProduct struct {\n\tName        string `form:\"name\" json:\"name\" xml:\"name\" binding:\"required\"`\n\tDescription string `form:\"description\" json:\"description\" xml:\"description\" binding:\"required\"`\n\tPrice       int    `form:\"price\" json:\"price\" xml:\"price\" binding:\"required\"`\n\tStock       int    `form:\"stock\" json:\"stock\" xml:\"stock\" binding:\"required\"`\n}\n\nfunc CreatedProductPagedResponse(request *http.Request, products []models.Product, page, page_size, count int, commentsCount []int) interface{} {\n\tvar resources = make([]interface{}, len(products))\n\tfor index, product := range products {\n\t\tresources[index] = CreateProductDto(&product, commentsCount[index])\n\t}\n\treturn CreatePagedResponse(request, resources, \"products\", page, page_size, count)\n}\n\nfunc CreateProductDto(product *models.Product, commentCount int) map[string]interface{} {\n\n\tvar tags = make([]map[string]interface{}, len(product.Tags))\n\tvar categories = make([]map[string]interface{}, len(product.Categories))\n\tvar images = make([]string, len(product.Images))\n\n\tfor index, tag := range product.Tags {\n\t\ttags[index] = map[string]interface{}{\n\t\t\t\"id\":   tag.ID,\n\t\t\t\"name\": tag.Name,\n\t\t\t\"slug\": tag.Slug,\n\t\t}\n\t}\n\n\tfor index, category := range product.Categories {\n\t\tcategories[index] = map[string]interface{}{\n\t\t\t\"id\":   category.ID,\n\t\t\t\"name\": category.Name,\n\t\t\t\"slug\": category.Slug,\n\t\t}\n\t}\n\treplaceAllFlag := -1\n\tfor index, image := range product.Images {\n\t\timages[index] = strings.Replace(image.FilePath, \"\\\\\", \"/\", replaceAllFlag)\n\t}\n\n\tfor index, tag := range product.Tags {\n\t\ttags[index] = map[string]interface{}{\n\t\t\t\"id\":   tag.ID,\n\t\t\t\"name\": tag.Name,\n\t\t\t\"slug\": tag.Slug,\n\t\t}\n\t}\n\n\tresult := map[string]interface{}{\n\t\t\"id\":         product.ID,\n\t\t\"name\":       product.Name,\n\t\t\"slug\":       product.Slug,\n\t\t\"price\":      product.Price,\n\t\t\"stock\":      product.Stock,\n\t\t\"tags\":       tags,\n\t\t\"categories\": categories,\n\t\t\"image_urls\": images,\n\t\t\"created_at\": product.CreatedAt.UTC().Format(\"2006-01-02T15:04:05.999Z\"),\n\t\t\"updated_at\": product.UpdatedAt.UTC().Format(time.RFC3339Nano),\n\t}\n\n\tif commentCount >= 0 {\n\t\t// \"comments_count\": product.CommentsCount,\n\t\tresult[\"comments_count\"] = commentCount\n\t}\n\treturn result\n}\n\nfunc CreateProductDetailsDto(product models.Product) map[string]interface{} {\n\tresult := CreateProductDto(&product, -1)\n\tresult[\"description\"] = product.Description\n\tcomments := make([]map[string]interface{}, len(product.Comments))\n\tfor index, comment := range product.Comments {\n\t\tcomments[index] = GetSummary(&comment, true, false)\n\t}\n\n\tresult[\"comments\"] = comments\n\treturn result\n}\nfunc CreateProductCreatedDto(product models.Product) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(CreateProductDetailsDto(product), \"Product crated successfully\")\n}\n"
  },
  {
    "path": "dtos/shared.go",
    "content": "package dtos\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"gopkg.in/go-playground/validator.v8\"\n\t\"math\"\n\t\"net/http\"\n)\n\ntype BaseDto struct {\n\tSuccess      bool     `json:\"success\"`\n\tFullMessages []string `json:\"full_messages\"`\n}\n\ntype ErrorDto struct {\n\tBaseDto\n\tErrors map[string]interface{} `json:\"errors\"`\n}\n\nfunc CreatePageMeta(request *http.Request, loadedItemsCount, page, page_size, totalItemsCount int) map[string]interface{} {\n\tpage_meta := map[string]interface{}{}\n\tpage_meta[\"offset\"] = (page - 1) * page_size\n\tpage_meta[\"requested_page_size\"] = page_size\n\tpage_meta[\"current_page_number\"] = page\n\tpage_meta[\"current_items_count\"] = loadedItemsCount\n\n\tpage_meta[\"prev_page_number\"] = 1\n\ttotal_pages_count := int(math.Ceil(float64(totalItemsCount) / float64(page_size)))\n\tpage_meta[\"total_pages_count\"] = total_pages_count\n\n\tif page < total_pages_count {\n\t\tpage_meta[\"has_next_page\"] = true\n\t\tpage_meta[\"next_page_number\"] = page + 1\n\t} else {\n\t\tpage_meta[\"has_next_page\"] = false\n\t\tpage_meta[\"next_page_number\"] = 1\n\t}\n\tif page > 1 {\n\t\tpage_meta[\"prev_page_number\"] = page - 1\n\t} else {\n\t\tpage_meta[\"has_prev_page\"] = false\n\t\tpage_meta[\"prev_page_number\"] = 1\n\t}\n\n\tpage_meta[\"next_page_url\"] = fmt.Sprintf(\"%v?page=%d&page_size=%d\", request.URL.Path, page_meta[\"next_page_number\"], page_meta[\"requested_page_size\"])\n\tpage_meta[\"prev_page_url\"] = fmt.Sprintf(\"%s?page=%d&page_size=%d\", request.URL.Path, page_meta[\"prev_page_number\"], page_meta[\"requested_page_size\"])\n\n\tresponse := gin.H{\n\t\t\"success\":   true,\n\t\t\"page_meta\": page_meta,\n\t}\n\n\treturn response\n}\n\nfunc CreatePagedResponse(request *http.Request, resources []interface{}, resource_name string, page, page_size, totalItemsCount int) map[string]interface{} {\n\n\tresponse := CreatePageMeta(request, len(resources), page, page_size, totalItemsCount)\n\tresponse[resource_name] = resources\n\treturn response\n}\n\nfunc CreateDetailedErrorDto(key string, err error) map[string]interface{} {\n\treturn map[string]interface{}{\n\t\t\"success\":       false,\n\t\t\"full_messages\": []string{fmt.Sprintf(\"s -> %v\", key, err.Error())},\n\t\t\"errors\":        err,\n\t}\n}\n\nfunc CreateErrorDtoWithMessage(message string) map[string]interface{} {\n\treturn map[string]interface{}{\n\t\t\"success\":       false,\n\t\t\"full_messages\": []string{message},\n\t}\n}\n\n// This should only be called when we have an Error that is returned from a ShouldBind which contains a lot of information\n// other kind of errors should use other functions such as CreateDetailedErrorDto\nfunc CreateBadRequestErrorDto(err error) ErrorDto {\n\tres := ErrorDto{}\n\tres.Errors = make(map[string]interface{})\n\terrs := err.(validator.ValidationErrors)\n\tres.FullMessages = make([]string, len(errs))\n\tcount := 0\n\tfor _, v := range errs {\n\t\tif v.ActualTag == \"required\" {\n\t\t\tvar message = fmt.Sprintf(\"%v is required\", v.Field)\n\t\t\tres.Errors[v.Field] = message\n\t\t\tres.FullMessages[count] = message\n\t\t} else {\n\t\t\tvar message = fmt.Sprintf(\"%v has to be %v\", v.Field, v.ActualTag)\n\t\t\tres.Errors[v.Field] = message\n\t\t\tres.FullMessages = append(res.FullMessages, message)\n\t\t}\n\t\tcount++\n\t}\n\treturn res\n}\n\nfunc CreateSuccessDto(result map[string]interface{}) map[string]interface{} {\n\tresult[\"success\"] = true\n\treturn result\n}\n\nfunc CreateSuccessWithMessageDto(message string) interface{} {\n\treturn CreateSuccessWithMessagesDto([]string{message})\n}\n\nfunc CreateSuccessWithMessagesDto(messages []string) interface{} {\n\treturn gin.H{\n\t\t\"success\":       true,\n\t\t\"full_messages\": messages,\n\t}\n}\n\nfunc CreateSuccessWithDtoAndMessagesDto(data map[string]interface{}, messages []string) map[string]interface{} {\n\tdata[\"success\"] = true\n\tdata[\"full_messages\"] = messages\n\treturn data\n}\nfunc CreateSuccessWithDtoAndMessageDto(data map[string]interface{}, message string) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessagesDto(data, []string{message})\n}\n"
  },
  {
    "path": "dtos/tags.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"strings\"\n)\n\ntype CreateTag struct {\n\tName        string `form:\"name\" binding:\"required\"`\n\tDescription string `form:\"description\" binding:\"required\"`\n}\n\nfunc CreateTagListMapDto(tags []models.Tag) map[string]interface{} {\n\tresult := map[string]interface{}{}\n\tvar t = make([]interface{}, len(tags))\n\tfor i := 0; i < len(tags); i++ {\n\t\tt[i] = CreateTagDto(tags[i])\n\t}\n\tresult[\"tags\"] = t\n\treturn CreateSuccessDto(result)\n}\n\nfunc CreateTagListDto(tags []models.Tag) []interface{} {\n\tvar t = make([]interface{}, len(tags))\n\tfor i := 0; i < len(tags); i++ {\n\t\tt[i] = CreateTagDto(tags[i])\n\t}\n\treturn t\n}\n\nfunc CreateTagDto(tag models.Tag) map[string]interface{} {\n\tvar imageUrls = make([]string, len(tag.Images))\n\treplaceAllFlag := -1\n\tfor i := 0; i < len(tag.Images); i++ {\n\t\timageUrls[i] = strings.Replace(tag.Images[i].FilePath, \"\\\\\", \"/\", replaceAllFlag)\n\t}\n\treturn map[string]interface{}{\n\t\t\"id\":          tag.ID,\n\t\t\"name\":        tag.Name,\n\t\t\"description\": tag.Description,\n\t\t\"image_urls\":  imageUrls,\n\t}\n}\n\nfunc CreateTagCreatedDto(tag models.Tag) map[string]interface{} {\n\treturn CreateSuccessWithDtoAndMessageDto(CreateTagDto(tag), \"Tag created successfully\")\n}\n"
  },
  {
    "path": "dtos/users.go",
    "content": "package dtos\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\ntype RegisterRequestDto struct {\n\tUsername             string `form:\"username\" json:\"username\" xml:\"username\"  binding:\"required\"`\n\tFirstName            string `form:\"first_name\" json:\"first_name\" xml:\"first_name\" binding:\"required\"`\n\tLastName             string `form:\"last_name\" json:\"last_name\" xml:\"last_name\" binding:\"required\"`\n\tEmail                string `form:\"email\" json:\"email\" xml:\"email\" binding:\"required\"`\n\tPassword             string `form:\"password\" json:\"password\" xml:\"password\" binding:\"required\"`\n\tPasswordConfirmation string `form:\"password_confirmation\" json:\"password_confirmation\" xml:\"password-confirmation\" binding:\"required\"`\n}\n\ntype LoginRequestDto struct {\n\t// Username string `form:\"username\" json:\"username\" xml:\"username\" binding:\"exists,username\"`\n\tUsername string `form:\"username\" json:\"username\" xml:\"username\" binding:\"required\"`\n\tPassword string `form:\"password\"json:\"password\" binding:\"exists,min=8,max=255\"`\n\n\tuserModel models.User `json:\"-\"`\n}\n\nfunc CreateLoginSuccessful(user *models.User) map[string]interface{} {\n\tvar roles = make([]string, len(user.Roles))\n\n\tfor i := 0; i < len(user.Roles); i++ {\n\t\troles[i] = user.Roles[i].Name\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"success\": true,\n\t\t\"token\":   user.GenerateJwtToken(),\n\t\t\"user\": map[string]interface{}{\n\t\t\t\"username\": user.Username,\n\t\t\t\"id\":       user.ID,\n\t\t\t\"roles\":    roles,\n\t\t},\n\t}\n}\n\nfunc GetUserBasicInfo(user models.User) map[string]interface{} {\n\treturn map[string]interface{}{\n\t\t\"id\":       user.ID,\n\t\t\"username\": user.Username,\n\t}\n}\n"
  },
  {
    "path": "infrastructure/db.go",
    "content": "package infrastructure\n\nimport (\n\t\"fmt\"\n\t\"github.com/jinzhu/gorm\"\n\t\"path\"\n\n\t_ \"github.com/jinzhu/gorm/dialects/mysql\"\n\t_ \"github.com/jinzhu/gorm/dialects/postgres\"\n\t_ \"github.com/jinzhu/gorm/dialects/sqlite\"\n\t// import _ \"github.com/jinzhu/gorm/dialects/mssql\"\n\t\"os\"\n)\n\ntype Database struct {\n\t*gorm.DB\n}\n\nvar DB *gorm.DB\n\n// Opening a database and save the reference to `Database` struct.\nfunc OpenDbConnection() *gorm.DB {\n\n\tdialect := os.Getenv(\"DB_DIALECT\")\n\tusername := os.Getenv(\"DB_USER\")\n\tpassword := os.Getenv(\"DB_PASSWORD\")\n\tdbName := os.Getenv(\"DB_NAME\")\n\thost := os.Getenv(\"DB_HOST\")\n\tvar db *gorm.DB\n\tvar err error\n\tif dialect == \"sqlite3\" {\n\t\tdb, err = gorm.Open(\"sqlite3\", path.Join(\".\", \"app.db\"))\n\t} else {\n\t\t// db, err := gorm.Open(\"mysql\", \"root:root@localhost/go_api_shop_gonc?charset=utf8\")\n\t\tdatabaseUrl := fmt.Sprintf(\"host=%s user=%s password=%s dbname=%s sslmode=disable \", host, username, password, dbName)\n\t\tdb, err = gorm.Open(dialect, databaseUrl)\n\t}\n\n\tif err != nil {\n\t\tfmt.Println(\"db err: \", err)\n\t\tos.Exit(-1)\n\t}\n\n\tdb.DB().SetMaxIdleConns(10)\n\tdb.LogMode(true)\n\tDB = db\n\treturn DB\n}\n\n// Delete the database after running testing cases.\nfunc RemoveDb(db *gorm.DB) error {\n\tdb.Close()\n\terr := os.Remove(path.Join(\".\", \"app.db\"))\n\treturn err\n}\n\n// Using this function to get a connection, you can create your connection pool here.\nfunc GetDb() *gorm.DB {\n\treturn DB\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-contrib/cors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/jinzhu/gorm\"\n\t\"github.com/joho/godotenv\"\n\t\"github.com/melardev/GoGonicEcommerceApi/controllers\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/middlewares\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"github.com/melardev/GoGonicEcommerceApi/seeds\"\n\t\"os\"\n)\n\nfunc drop(db *gorm.DB) {\n\tdb.DropTableIfExists(\n\t\t&models.FileUpload{},\n\t\t&models.Comment{},\n\t\t&models.OrderItem{}, &models.Order{}, &models.Address{},\n\t\t&models.ProductCategory{}, &models.ProductTag{},\n\t\t&models.Tag{}, &models.Category{},\n\t\t&models.Product{},\n\t\t&models.UserRole{}, &models.Role{}, &models.User{})\n}\n\nfunc migrate(database *gorm.DB) {\n\n\tdatabase.AutoMigrate(&models.Address{})\n\n\tdatabase.AutoMigrate(&models.Category{})\n\tdatabase.AutoMigrate(&models.Comment{})\n\n\tdatabase.AutoMigrate(&models.Order{})\n\tdatabase.AutoMigrate(&models.OrderItem{})\n\n\tdatabase.AutoMigrate(&models.Product{})\n\tdatabase.AutoMigrate(&models.ProductCategory{})\n\n\tdatabase.AutoMigrate(&models.Role{})\n\tdatabase.AutoMigrate(&models.UserRole{})\n\n\tdatabase.AutoMigrate(&models.Tag{})\n\tdatabase.AutoMigrate(&models.ProductTag{})\n\n\tdatabase.AutoMigrate(&models.User{})\n\n\tdatabase.AutoMigrate(&models.FileUpload{})\n}\n\nfunc addDbConstraints(database *gorm.DB) {\n\t// TODO: it is well known GORM does not add foreign keys even after using ForeignKey in struct, but, why manually does not work neither ?\n\n\tdialect := database.Dialect().GetName() // mysql, sqlite3\n\tif dialect != \"sqlite3\" {\n\t\tdatabase.Model(&models.Comment{}).AddForeignKey(\"product_id\", \"products(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(&models.Comment{}).AddForeignKey(\"user_id\", \"users(id)\", \"CASCADE\", \"CASCADE\")\n\n\t\tdatabase.Model(&models.Order{}).AddForeignKey(\"user_id\", \"users(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(&models.Order{}).AddForeignKey(\"address_id\", \"addresses(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(&models.OrderItem{}).AddForeignKey(\"order_id\", \"orders(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(&models.OrderItem{}).AddForeignKey(\"user_id\", \"users(id)\", \"CASCADE\", \"CASCADE\")\n\n\t\tdatabase.Model(&models.Address{}).AddForeignKey(\"user_id\", \"users(id)\", \"CASCADE\", \"CASCADE\")\n\n\t\tdatabase.Model(&models.UserRole{}).AddForeignKey(\"user_id\", \"users(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(&models.UserRole{}).AddForeignKey(\"role_id\", \"roles(id)\", \"CASCADE\", \"CASCADE\")\n\n\t\tdatabase.Table(\"products_tags\").AddForeignKey(\"product_id\", \"products(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Table(\"products_tags\").AddForeignKey(\"tag_id\", \"tags(id)\", \"CASCADE\", \"CASCADE\")\n\n\t\tdatabase.Model(models.ProductCategory{}).AddForeignKey(\"product_id\", \"products(id)\", \"CASCADE\", \"CASCADE\")\n\t\tdatabase.Model(models.ProductCategory{}).AddForeignKey(\"category_id\", \"categories(id)\", \"CASCADE\", \"CASCADE\")\n\t} else if dialect == \"sqlite3\" {\n\t\tdatabase.Table(\"comments\").AddIndex(\"comments__idx_product_id\", \"product_id\")\n\t\tdatabase.Table(\"comments\").AddIndex(\"comments__idx_user_id\", \"user_id\")\n\n\t\tdatabase.Table(\"ratings\").AddIndex(\"ratings__idx_user_id\", \"user_id\")\n\t\tdatabase.Table(\"ratings\").AddIndex(\"ratings__idx_product_id\", \"product_id\")\n\n\t\tdatabase.Model(&models.Comment{}).AddIndex(\"comments__idx_created_at\", \"created_at\")\n\n\t}\n\n\tdatabase.Model(&models.UserRole{}).AddIndex(\"user_roles__idx_user_id\", \"user_id\")\n\tdatabase.Table(\"products_tags\").AddIndex(\"products_tags__idx_product_id\", \"product_id\")\n}\nfunc create(database *gorm.DB) {\n\tdrop(database)\n\tmigrate(database)\n\taddDbConstraints(database)\n}\n\nfunc main() {\n\n\te := godotenv.Load() //Load .env file\n\tif e != nil {\n\t\tfmt.Print(e)\n\t}\n\tprintln(os.Getenv(\"DB_DIALECT\"))\n\n\tdatabase := infrastructure.OpenDbConnection()\n\n\tdefer database.Close()\n\targs := os.Args\n\tif len(args) > 1 {\n\t\tfirst := args[1]\n\t\tsecond := \"\"\n\t\tif len(args) > 2 {\n\t\t\tsecond = args[2]\n\t\t}\n\n\t\tif first == \"create\" {\n\t\t\tcreate(database)\n\t\t} else if first == \"seed\" {\n\t\t\tseeds.Seed()\n\t\t\tos.Exit(0)\n\t\t} else if first == \"migrate\" {\n\t\t\tmigrate(database)\n\t\t}\n\n\t\tif second == \"seed\" {\n\t\t\tseeds.Seed()\n\t\t\tos.Exit(0)\n\t\t} else if first == \"migrate\" {\n\t\t\tmigrate(database)\n\t\t}\n\n\t\tif first != \"\" && second == \"\" {\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\tmigrate(database)\n\n\t// gin.New() - new gin Instance with no middlewares\n\t// goGonicEngine.Use(gin.Logger())\n\t// goGonicEngine.Use(gin.Recovery())\n\tgoGonicEngine := gin.Default() // gin with the Logger and Recovery Middlewares attached\n\t// Allow all Origins\n\tgoGonicEngine.Use(cors.Default())\n\n\tgoGonicEngine.Use(middlewares.Benchmark())\n\n\t// goGonicEngine.Use(middlewares.Cors())\n\n\tgoGonicEngine.Use(middlewares.UserLoaderMiddleware())\n\tgoGonicEngine.Static(\"/static\", \"./static\")\n\tapiRouteGroup := goGonicEngine.Group(\"/api\")\n\n\tcontrollers.RegisterUserRoutes(apiRouteGroup.Group(\"/users\"))\n\tcontrollers.RegisterProductRoutes(apiRouteGroup.Group(\"/products\"))\n\tcontrollers.RegisterCommentRoutes(apiRouteGroup.Group(\"/\"))\n\tcontrollers.RegisterPageRoutes(apiRouteGroup.Group(\"/\"))\n\tcontrollers.RegisterAddressesRoutes(apiRouteGroup.Group(\"/users\"))\n\tcontrollers.RegisterTagRoutes(apiRouteGroup.Group(\"/tags\"))\n\tcontrollers.RegisterCategoryRoutes(apiRouteGroup.Group(\"/categories\"))\n\tcontrollers.RegisterOrderRoutes(apiRouteGroup.Group(\"/orders\"))\n\n\tgoGonicEngine.Run(\":8080\") // listen and serve on 0.0.0.0:8080\n}\n"
  },
  {
    "path": "middlewares/auth.go",
    "content": "package middlewares\n\nimport (\n\t\"fmt\"\n\t\"github.com/dgrijalva/jwt-go\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc EnforceAuthenticatedMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tuser, exists := c.Get(\"currentUser\")\n\t\tif exists && user.(models.User).ID != 0 {\n\t\t\treturn\n\t\t} else {\n\t\t\terr, _ := c.Get(\"authErr\")\n\t\t\t_ = c.AbortWithError(http.StatusUnauthorized, err.(error))\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc UserLoaderMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tbearer := c.Request.Header.Get(\"Authorization\")\n\t\tif bearer != \"\" {\n\t\t\tjwtParts := strings.Split(bearer, \" \")\n\t\t\tif len(jwtParts) == 2 {\n\t\t\t\tjwtEncoded := jwtParts[1]\n\n\t\t\t\ttoken, err := jwt.Parse(jwtEncoded, func(token *jwt.Token) (interface{}, error) {\n\t\t\t\t\t// Theorically we have also to validate the algorithm\n\t\t\t\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected signin method %v\", token.Header[\"alg\"])\n\t\t\t\t\t}\n\t\t\t\t\tsecret := []byte(os.Getenv(\"JWT_SECRET\"))\n\t\t\t\t\treturn secret, nil\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tprintln(err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {\n\t\t\t\t\tuserId := uint(claims[\"user_id\"].(float64))\n\t\t\t\t\tfmt.Printf(\"[+] Authenticated request, authenticated user id is %d\\n\", userId)\n\n\t\t\t\t\tvar user models.User\n\t\t\t\t\tif userId != 0 {\n\t\t\t\t\t\tdatabase := infrastructure.GetDb()\n\t\t\t\t\t\t// We always need the Roles to be loaded to make authorization decisions based on Roles\n\t\t\t\t\t\tdatabase.Preload(\"Roles\").First(&user, userId)\n\t\t\t\t\t}\n\n\t\t\t\t\tc.Set(\"currentUser\", user)\n\t\t\t\t\tc.Set(\"currentUserId\", user.ID)\n\t\t\t\t} else {\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middlewares/benchmark.go",
    "content": "package middlewares\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"math\"\n\t\"time\"\n)\n\nfunc Benchmark() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tstart := time.Now()\n\t\tc.Next()\n\t\telapsed := time.Since(start)\n\t\tfmt.Printf(\"Request took %v milliseconds\\n\", float64(elapsed.Nanoseconds())/math.Pow(float64(10), float64(6)))\n\t}\n}\n"
  },
  {
    "path": "middlewares/cors.go",
    "content": "package middlewares\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Cors() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Writer.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tc.Writer.Header().Set(\"Access-Control-Allow-Headers\", \"Content-Type,Token\")\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "models/address.go",
    "content": "package models\n\nimport \"github.com/jinzhu/gorm\"\n\ntype Address struct {\n\tgorm.Model\n\tStreetAddress string `gorm:\"not null\"`\n\tCity          string `gorm:\"not null\"`\n\tCountry       string `gorm:\"not null\"`\n\tZipCode       string `gorm:\"not null\"`\n\tFirstName     string `gorm:\"not null\"`\n\tLastName      string `gorm:\"not null\"`\n\n\tUser   User    `gorm:\"association_foreignkey:UserId:\"`\n\tUserId uint    `gorm:\"default:null\"` // Guest users may place an order, so they should be able to create an address with nullable UserId\n\tOrders []Order `gorm:\"foreignKey:AddressId\"`\n}\n"
  },
  {
    "path": "models/category.go",
    "content": "package models\n\nimport (\n\t\"github.com/gosimple/slug\"\n\t\"github.com/jinzhu/gorm\"\n)\n\ntype Category struct {\n\tgorm.Model\n\tName        string       `gorm:\"not null\"`\n\tDescription string       `gorm:\"default:null\"`\n\tSlug        string       `gorm:\"unique_index\"`\n\tProducts    []Product    `gorm:\"many2many:products_categories;\"`\n\tImages      []FileUpload `gorm:\"foreignKey:CategoryId\"`\n\tIsNewRecord bool         `gorm:\"-;default:false\"`\n}\n\nfunc (a *Category) BeforeSave() (err error) {\n\ta.Slug = slug.Make(a.Name)\n\treturn\n}\n"
  },
  {
    "path": "models/comment.go",
    "content": "package models\n\nimport (\n\t\"github.com/jinzhu/gorm\"\n)\n\ntype Comment struct {\n\tgorm.Model\n\tContent   string  `gorm:\"size:2048\"`\n\tRating    int     `gorm:\"default:null\"`\n\tProduct   Product `gorm:\"foreignkey:ProductId\"`\n\tProductId uint    `gorm:\"not null\"`\n\tUser      User    `gorm:\"foreignkey:UserId\"`\n\tUserId    uint    `gorm:\"not null\"`\n}\n"
  },
  {
    "path": "models/file_upload.go",
    "content": "package models\n\nimport \"github.com/jinzhu/gorm\"\n\ntype FileUpload struct {\n\tgorm.Model\n\tFilename     string\n\tFilePath     string\n\tOriginalName string\n\tFileSize     uint\n\n\tTag   Tag  `gorm:\"association_foreignkey:TagId\"`\n\tTagId uint `gorm:\"default:null\"`\n\n\tCategory   Category `gorm:\"association_foreignkey:CategoryId\"`\n\tCategoryId uint     `gorm:\"default:null\"`\n\n\tProduct   Category `gorm:\"association_foreignkey:ProductId\"`\n\tProductId uint     `gorm:\"default:null\"`\n}\n\n// Scopes, not used\nfunc TagImages(db *gorm.DB) *gorm.DB {\n\treturn db.Where(\"type = ?\", \"TagImage\")\n}\n\nfunc CategoryImages(db *gorm.DB) *gorm.DB {\n\treturn db.Where(\"type = ?\", \"CategoryImage\")\n}\n\nfunc ProductImages(db *gorm.DB) *gorm.DB {\n\treturn db.Where(\"type = ?\", \"ProductImage\")\n}\n\n// db.Scopes(CategoryImages, ProductImages).Find(&images)\n"
  },
  {
    "path": "models/order.go",
    "content": "package models\n\nimport \"github.com/jinzhu/gorm\"\n\ntype Order struct {\n\tgorm.Model\n\tOrderStatus    int `gorm:\"default:0\"`\n\tTrackingNumber string\n\n\tOrderItems []OrderItem `gorm:\"foreignKey:OrderId\"`\n\n\tAddress   Address `gorm:\"association_foreignkey:AddressId:\"`\n\tAddressId uint\n\n\tUser            User `gorm:\"foreignKey:UserId:\"`\n\tUserId          uint `gorm:\"default:null\"`\n\tOrderItemsCount int  `gorm:\"-\"`\n}\n\nfunc (order *Order) GetOrderStatusAsString() string {\n\tswitch order.OrderStatus {\n\tcase 0:\n\t\treturn \"processed\"\n\tcase 1:\n\t\treturn \"delivered\"\n\tcase 2:\n\t\treturn \"shipped\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n"
  },
  {
    "path": "models/order_item.go",
    "content": "package models\n\nimport \"github.com/jinzhu/gorm\"\n\ntype OrderItem struct {\n\tgorm.Model\n\tOrder   Order\n\tOrderId uint `gorm:\"not null\"`\n\n\tProduct   Product\n\tProductId uint `gorm:\"not null\"`\n\n\tSlug        string `gorm:\"not null\"`\n\tProductName string `gorm:\"not null\"`\n\tPrice       int    `gorm:\"not null\"`\n\tQuantity    int    `gorm:\"not null\"`\n\n\tUser   User `gorm:\"association_foreignkey:UserId:\"`\n\tUserId uint `gorm:\"default:null\"`\n}\n"
  },
  {
    "path": "models/product.go",
    "content": "package models\n\nimport (\n\t\"github.com/gosimple/slug\"\n\t\"github.com/jinzhu/gorm\"\n)\n\ntype Product struct {\n\tgorm.Model\n\tName        string       `gorm:\"size:280;not null\"`\n\tDescription string       `gorm:\"not null\"`\n\tSlug        string       `gorm:\"unique_index;not null\"`\n\tPrice       int          `gorm:\"not null\"`\n\tStock       int          `gorm:\"not null\"`\n\tTags        []Tag        `gorm:\"many2many:products_tags;\"`\n\tProductTags []ProductTag `gorm:\"foreignkey:ProductId\"`\n\n\tCategories        []Category        `gorm:\"many2many:products_categories;\"`\n\tProductCategories []ProductCategory `gorm:\"foreignkey:ProductId\"`\n\n\tComments      []Comment    `gorm:\"foreignKey:ProductId\"`\n\tImages        []FileUpload `gorm:\"foreignKey:ProductId\"`\n\tCommentsCount int          `gorm:\"-\"`\n}\n\nfunc (product *Product) BeforeSave() (err error) {\n\tproduct.Slug = slug.Make(product.Name)\n\treturn\n}\n"
  },
  {
    "path": "models/product_category.go",
    "content": "package models\n\ntype ProductCategory struct {\n\tCategory   User `gorm:\"association_foreignkey:CategoryId\"`\n\tCategoryId uint\n\tProduct    Product `gorm:\"association_foreignkey:ProductId\"`\n\tProductId  uint\n}\n\nfunc (*ProductCategory) TableName() string {\n\treturn \"products_categories\"\n}\n"
  },
  {
    "path": "models/product_tag.go",
    "content": "package models\n\ntype ProductTag struct {\n\tTag       User `gorm:\"association_foreignkey:TagId\"`\n\tTagId     uint\n\tProduct   Product `gorm:\"association_foreignkey:ProductId\"`\n\tProductId uint\n}\n\nfunc (*ProductTag) TableName() string {\n\treturn \"products_tags\"\n}\n"
  },
  {
    "path": "models/role.go",
    "content": "package models\n\nimport \"github.com/jinzhu/gorm\"\n\ntype Role struct {\n\tgorm.Model\n\tName        string\n\tDescription string\n\tUsers       []User     `gorm:\"many2many:users_roles;\"`\n\tUserRoles   []UserRole `gorm:\"foreignkey:RoleId\"`\n}\n\ntype UserRole struct {\n\tUser   User `gorm:\"association_foreignkey:UserId\"`\n\tUserId uint\n\tRole   User `gorm:\"association_foreignkey:RoleId\"`\n\tRoleId uint\n}\n\nfunc (UserRole) TableName() string {\n\treturn \"users_roles\"\n}\n\nfunc Any(roles []Role, f func(Role) bool) bool {\n\tfor _, role := range roles {\n\t\tif f(role) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "models/tag.go",
    "content": "package models\n\nimport (\n\t\"github.com/gosimple/slug\"\n\t\"github.com/jinzhu/gorm\"\n)\n\ntype Tag struct {\n\tgorm.Model\n\tName        string       `gorm:\"not null\"`\n\tDescription string       `gorm:\"default:null\"`\n\tSlug        string       `gorm:\"unique_index\"`\n\tProducts    []Product    `gorm:\"many2many:products_tags;\"`\n\tImages      []FileUpload `gorm:\"foreignKey:TagId\"`\n\tIsNewRecord bool         `gorm:\"-;default:false\"` // Virtual Field, so it is not persisted in the Db. This is used in FirstOrCreate()\n}\n\nfunc (a *Tag) BeforeSave() (err error) {\n\ta.Slug = slug.Make(a.Name)\n\treturn\n}\n"
  },
  {
    "path": "models/user.go",
    "content": "package models\n\nimport (\n\t\"errors\"\n\t\"github.com/dgrijalva/jwt-go\"\n\t\"github.com/jinzhu/gorm\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"os\"\n\t\"time\"\n)\n\ntype User struct {\n\tgorm.Model\n\t//Id           uint    `gorm:\"primary_key\"`\n\tFirstName string `gorm:\"varchar(255);not null\"`\n\tLastName  string `gorm:\"varchar(255);not null\"`\n\tUsername  string `gorm:\"column:username\"`\n\tEmail     string `gorm:\"column:email;unique_index\"`\n\tPassword  string `gorm:\"column:password;not null\"`\n\n\tComments []Comment `gorm:\"foreignkey:UserId\"`\n\n\tRoles     []Role     `gorm:\"many2many:users_roles;\"`\n\tUserRoles []UserRole `gorm:\"foreignkey:UserId\"`\n}\n\n// What's bcrypt? https://en.wikipedia.org/wiki/Bcrypt\n// Golang bcrypt doc: https://godoc.org/golang.org/x/crypto/bcrypt\n// You can change the value in bcrypt.DefaultCost to adjust the security index.\n// \terr := userModel.setPassword(\"password0\")\nfunc (u *User) SetPassword(password string) error {\n\tif len(password) == 0 {\n\t\treturn errors.New(\"password should not be empty\")\n\t}\n\tbytePassword := []byte(password)\n\t// Make sure the second param `bcrypt generator cost` between [4, 32)\n\tpasswordHash, _ := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost)\n\tu.Password = string(passwordHash)\n\treturn nil\n}\n\n// Database will only save the hashed string, you should check it by util function.\n// \tif err := serModel.checkPassword(\"password0\"); err != nil { password error }\nfunc (u *User) IsValidPassword(password string) error {\n\tbytePassword := []byte(password)\n\tbyteHashedPassword := []byte(u.Password)\n\treturn bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword)\n}\n\nfunc (user *User) BeforeSave(db *gorm.DB) (err error) {\n\tif len(user.Roles) == 0 {\n\t\t// role := Role{}\n\t\tuserRole := Role{}\n\t\t// db.Model(&role).Where(\"name = ?\", \"ROLE_USER\").First(&userRole)\n\t\tdb.Model(&Role{}).Where(\"name = ?\", \"ROLE_USER\").First(&userRole)\n\t\t//db.Where(&models.Role{Name: \"ROLE_USER\"}).Attrs(models.Role{Description: \"For standard Users\"}).FirstOrCreate(&userRole)\n\t\tuser.Roles = append(user.Roles, userRole)\n\t}\n\treturn\n}\n\n// Generate JWT token associated to this user\nfunc (user *User) GenerateJwtToken() string {\n\t// jwt.New(jwt.GetSigningMethod(\"HS512\"))\n\tjwt_token := jwt.New(jwt.SigningMethodHS512)\n\n\tvar roles []string\n\tfor _, role := range user.Roles {\n\t\troles = append(roles, role.Name)\n\t}\n\n\tjwt_token.Claims = jwt.MapClaims{\n\t\t\"user_id\":  user.ID,\n\t\t\"username\": user.Username,\n\t\t\"roles\":    roles,\n\t\t\"exp\":      time.Now().Add(time.Hour * 24 * 90).Unix(),\n\t}\n\t// Sign and get the complete encoded token as a string\n\ttoken, _ := jwt_token.SignedString([]byte(os.Getenv(\"JWT_SECRET\")))\n\treturn token\n}\n\nfunc (user *User) IsAdmin() bool {\n\tfor _, role := range user.Roles {\n\t\tif role.Name == \"ROLE_ADMIN\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\nfunc (user *User) IsNotAdmin() bool {\n\treturn !user.IsAdmin()\n}\n"
  },
  {
    "path": "seeds/seeder.go",
    "content": "package seeds\n\nimport (\n\t\"github.com/icrowley/fake\"\n\t\"github.com/jinzhu/gorm\"\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"math/rand\"\n\t\"time\"\n)\n\nfunc randomInt(min, max int) int {\n\n\treturn rand.Intn(max-min) + min\n}\n\nvar letterRunes = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\nfunc randomString(length int) string {\n\tb := make([]rune, length)\n\tfor i := range b {\n\t\tb[i] = letterRunes[rand.Intn(len(letterRunes))]\n\t}\n\treturn string(b)\n}\n\nfunc seedAdmin(db *gorm.DB) {\n\tcount := 0\n\tadminRole := models.Role{Name: \"ROLE_ADMIN\", Description: \"Only for admin\"}\n\tquery := db.Model(&models.Role{}).Where(\"name = ?\", \"ROLE_ADMIN\")\n\tquery.Count(&count)\n\n\tif count == 0 {\n\t\tdb.Create(&adminRole)\n\t} else {\n\t\tquery.First(&adminRole)\n\t}\n\n\tadminRoleUsers := 0\n\tvar adminUsers []models.User\n\tdb.Model(&adminRole).Related(&adminUsers, \"Users\")\n\n\tdb.Model(&models.User{}).Where(\"username = ?\", \"admin\").Count(&adminRoleUsers)\n\tif adminRoleUsers == 0 {\n\n\t\t// query.First(&adminRole) // First would fetch the Role admin because the query status name='ROLE_ADMIN'\n\t\tpassword, _ := bcrypt.GenerateFromPassword([]byte(\"password\"), bcrypt.DefaultCost)\n\t\t// Approach 1\n\t\tuser := models.User{FirstName: \"AdminFN\", LastName: \"AdminFN\", Email: \"admin@golang.com\", Username: \"admin\", Password: string(password)}\n\t\tuser.Roles = append(user.Roles, adminRole)\n\n\t\t// Do not try to update the adminRole\n\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&user)\n\n\t\t// Approach 2\n\t\t// user := models.User{FirstName: \"AdminFN\", LastName: \"AdminFN\", Email: \"admin@golang.com\", Username: \"admin\", Password: \"password\"}\n\t\t// user.Roles = append(user.Roles, adminRole)\n\t\t// db.NewRecord(user)\n\t\t// db.Set(\"gorm:association_autoupdate\", false).Save(&user)\n\n\t\tif db.Error != nil {\n\t\t\tprint(db.Error)\n\t\t}\n\t}\n}\n\nfunc seedUsers(db *gorm.DB) {\n\tcount := 0\n\trole := models.Role{Name: \"ROLE_USER\", Description: \"Only for standard users\"}\n\tq := db.Model(&models.Role{}).Where(\"name = ?\", \"ROLE_USER\")\n\tq.Count(&count)\n\n\tif count == 0 {\n\t\tdb.Create(&role)\n\t} else {\n\t\tq.First(&role)\n\t}\n\n\tvar standardUsers []models.User\n\tdb.Model(&role).Related(&standardUsers, \"Users\")\n\tusersCount := len(standardUsers)\n\tusersToSeed := 20\n\tusersToSeed -= usersCount\n\tif usersToSeed > 0 {\n\t\tfor i := 0; i < usersToSeed; i++ {\n\t\t\tpassword, _ := bcrypt.GenerateFromPassword([]byte(\"password\"), bcrypt.DefaultCost)\n\t\t\tuser := models.User{FirstName: fake.FirstName(), LastName: fake.LastName(), Email: fake.EmailAddress(), Username: fake.UserName(),\n\t\t\t\tPassword: string(password)}\n\t\t\t// No need to add the role as we did for seedAdmin, it is added by the BeforeSave hook\n\t\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&user)\n\t\t}\n\t}\n}\n\nfunc seedTags(db *gorm.DB) {\n\tvar tags [3]models.Tag\n\n\tdb.Where(&models.Tag{Name: \"Shoes\"}).Attrs(models.Tag{Description: \"Shoes for everyone\", IsNewRecord: true}).FirstOrCreate(&tags[0])\n\tdb.Where(models.Tag{Name: \"Jackets\"}).Attrs(models.Tag{Description: \"Jackets for everyone\", IsNewRecord: true}).FirstOrCreate(&tags[1])\n\tdb.Where(models.Tag{Name: \"Jeans\"}).Attrs(models.Tag{Description: \"Jeans for everyone\", IsNewRecord: true}).FirstOrCreate(&tags[2])\n\n\tfor _, tag := range tags {\n\t\tfor i := 0; i < randomInt(1, 3); i++ {\n\t\t\tif tag.IsNewRecord {\n\t\t\t\tdb.Create(&models.FileUpload{Filename: randomString(16) + \".png\", OriginalName: randomString(16) + \".png\",\n\t\t\t\t\tFilePath: \"/static/images/tags/\" + randomString(16) + \".png\", FileSize: 2500,\n\t\t\t\t\tTag: tag, TagId: tag.ID})\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc seedCategories(db *gorm.DB) {\n\tvar categories [3]models.Category\n\tdb.Where(models.Category{Name: \"Women\"}).Attrs(models.Category{Description: \"Clothes for women\", IsNewRecord: true}).FirstOrCreate(&categories[0])\n\tdb.Where(models.Category{Name: \"Men\"}).Attrs(models.Category{Description: \"Clothes for men\", IsNewRecord: true}).FirstOrCreate(&categories[1])\n\tdb.Where(models.Category{Name: \"Kids\"}).Attrs(models.Category{Description: \"Clothes for kids\", IsNewRecord: true}).FirstOrCreate(&categories[2])\n\n\tfor _, category := range categories {\n\t\tfor i := 0; i < randomInt(1, 3); i++ {\n\t\t\tif category.IsNewRecord {\n\t\t\t\tdb.Create(&models.FileUpload{Filename: randomString(16) + \".png\", OriginalName: randomString(16) + \".png\",\n\t\t\t\t\tFilePath: \"/static/images/categories/\" + randomString(16) + \".png\", FileSize: 2500,\n\t\t\t\t\tCategory: category, CategoryId: category.ID})\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc seedProducts(db *gorm.DB) {\n\tproductsCount := 0\n\tproductsToSeed := 20\n\tdb.Model(&models.Product{}).Count(&productsCount)\n\tproductsToSeed -= productsCount\n\n\tif productsToSeed > 0 {\n\t\trand.Seed(time.Now().Unix())\n\t\ttags := []models.Tag{}\n\t\tcategories := []models.Category{}\n\t\tdb.Find(&tags)\n\t\tdb.Find(&categories)\n\t\tfor i := 0; i < productsToSeed; i++ {\n\t\t\t// add a tag and a category for each product\n\t\t\t// faker.RandomInt(0, len(tags))[0]\n\t\t\ttagForProduct := tags[rand.Intn(len(tags))]\n\t\t\tcategoryForProduct := categories[rand.Intn(len(categories))]\n\n\t\t\tproduct := &models.Product{Name: fake.ProductName(), Description: fake.Paragraph(),\n\t\t\t\tStock: randomInt(100, 2000), Price: randomInt(50, 1000),\n\t\t\t\tTags: []models.Tag{tagForProduct}, Categories: []models.Category{categoryForProduct}}\n\t\t\tfor i := 0; i < randomInt(1, 4); i++ {\n\t\t\t\tproductImage := models.FileUpload{Filename: randomString(16) + \".png\", OriginalName: randomString(16) + \".png\",\n\t\t\t\t\tFilePath: \"/static/images/products/\" + randomString(16) + \".png\", FileSize: uint(randomInt(1000, 23000))}\n\t\t\t\tproduct.Images = append(product.Images, productImage)\n\t\t\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&product)\n\t\t\t}\n\n\t\t\t/*\n\t\t\t\tdb.Create(&models.FileUpload{Filename: randomString(16) + \".png\", OriginalName: randomString(16) + \".png\",\n\t\t\t\t\tFilePath: \"/static/images/tags\" + randomString(16) + \".png\", FileSize: 2500,\n\t\t\t\t\tTag: tag, TagId: tag.ID})\n\t\t\t*/\n\t\t}\n\t}\n}\n\nfunc seedComments(db *gorm.DB) {\n\tcommentsCount := 0\n\tcommentsToSeed := 20\n\n\tallUsers := []models.User{}\n\tallProducts := []models.Product{}\n\n\tdb.Model(&models.Comment{}).Count(&commentsCount)\n\tcommentsToSeed -= commentsCount\n\n\tif commentsToSeed > 0 {\n\t\trand.Seed(time.Now().Unix())\n\n\t\tdb.Find(&allProducts)\n\t\tdb.Find(&allUsers)\n\n\t\tfor i := 0; i < commentsToSeed; i++ {\n\t\t\tuserId := allUsers[rand.Intn(len(allUsers))].ID\n\t\t\tproductId := allProducts[rand.Intn(len(allProducts))].ID\n\t\t\tsentences := fake.SentencesN(randomInt(2, 6))\n\t\t\tvar comment models.Comment\n\n\t\t\tif rand.Float32() > 0.3 {\n\t\t\t\tcomment = models.Comment{Content: sentences, UserId: userId, ProductId: productId}\n\t\t\t} else {\n\t\t\t\t// Comment with rating\n\t\t\t\tcomment = models.Comment{Content: sentences, UserId: userId, ProductId: productId, Rating: randomInt(1, 5)}\n\t\t\t}\n\n\t\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&comment)\n\t\t}\n\t}\n}\nfunc seedAddresses(db *gorm.DB) {\n\taddressesCount := 0\n\taddressesToSeed := 20\n\n\tallUsers := []models.User{}\n\n\tdb.Model(&models.Address{}).Count(&addressesCount)\n\taddressesToSeed -= addressesCount\n\n\tif addressesToSeed > 0 {\n\t\trand.Seed(time.Now().Unix())\n\t\tdb.Find(&allUsers)\n\t\tvar address models.Address\n\n\t\tvar city string\n\t\tvar country string\n\t\tvar streetAddress string\n\t\tvar zipCode string\n\t\tfor i := 0; i < addressesToSeed; i++ {\n\t\t\tcity = fake.City()\n\t\t\tcountry = fake.Country()\n\t\t\tzipCode = fake.Zip()\n\t\t\tstreetAddress = fake.StreetAddress()\n\t\t\taddress = models.Address{ZipCode: zipCode, StreetAddress: streetAddress, Country: country, City: city}\n\t\t\tif rand.Float32() > 0.4 {\n\t\t\t\tuser := allUsers[rand.Intn(len(allUsers))]\n\t\t\t\taddress.UserId = user.ID\n\t\t\t\taddress.FirstName = user.FirstName\n\t\t\t\taddress.LastName = user.LastName\n\t\t\t} else {\n\t\t\t\taddress.FirstName = fake.FirstName()\n\t\t\t\taddress.LastName = fake.LastName()\n\t\t\t}\n\n\t\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&address)\n\t\t}\n\t}\n}\nfunc seedOrders(db *gorm.DB) {\n\tordersCount := 0\n\tordersToSeed := 20\n\n\tallAddresses := []models.Address{}\n\tallProducts := []models.Product{}\n\n\tdb.Model(&models.Order{}).Count(&ordersCount)\n\tordersToSeed -= ordersCount\n\n\tif ordersToSeed > 0 {\n\t\trand.Seed(time.Now().Unix())\n\t\t// Eager load the address's user association\n\t\tdb.Find(&allAddresses)\n\t\tdb.Find(&allProducts)\n\n\t\tfor i := 0; i < ordersToSeed; i++ {\n\t\t\taddress := allAddresses[rand.Intn(len(allAddresses))]\n\n\t\t\torder := models.Order{TrackingNumber: randomString(16), OrderStatus: randomInt(0, 3), AddressId: address.ID}\n\t\t\torderItemsForOrder := randomInt(2, 5)\n\t\t\tif rand.Float32() > 0.3 {\n\t\t\t\torder.UserId = address.UserId\n\t\t\t}\n\t\t\tfor j := 0; j < orderItemsForOrder; j++ {\n\t\t\t\tproduct := allProducts[rand.Intn(len(allProducts))]\n\t\t\t\torderItem := models.OrderItem{ProductName: product.Name, Price: product.Price, Slug: product.Slug,\n\t\t\t\t\tProductId: product.ID,\n\t\t\t\t\tUserId:    address.UserId, Quantity: randomInt(1, 8)}\n\n\t\t\t\torder.OrderItems = append(order.OrderItems, orderItem)\n\t\t\t}\n\n\t\t\tdb.Set(\"gorm:association_autoupdate\", false).Create(&order)\n\t\t}\n\t}\n}\n\nfunc Seed() {\n\tdb := infrastructure.GetDb()\n\trand.Seed(time.Now().UnixNano())\n\tseedAdmin(db)\n\tseedUsers(db)\n\tseedTags(db)\n\tseedCategories(db)\n\tseedProducts(db)\n\tseedComments(db)\n\tseedAddresses(db)\n\tseedOrders(db)\n}\n"
  },
  {
    "path": "services/addresses.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchAddressesPage(userId uint, page, pageSize int, includeUser bool) ([]models.Address, int) {\n\tvar addresses []models.Address\n\tvar totalAddressesCount int\n\tdatabase := infrastructure.GetDb()\n\tdatabase.Model(&models.Address{}).Where(&models.Address{UserId: uint(userId)}).Count(&totalAddressesCount)\n\tdatabase.Where(&models.Address{UserId: uint(userId)}).\n\t\tOffset((page - 1) * pageSize).Limit(pageSize).\n\t\tPreload(\"User\").\n\t\tFind(&addresses)\n\n\tif includeUser {\n\t\tvar userIds = make([]uint, len(addresses))\n\t\tvar users []models.User\n\t\tfor i := 0; i < len(addresses); i++ {\n\t\t\tuserIds[i] = addresses[i].UserId\n\t\t}\n\t\tdatabase.Select([]string{\"id\", \"username\"}).Where(userIds).Find(&users)\n\n\t\t// If the user gets deleted and the comment is still in the database we may have less users than addresses\n\t\t// Another scenario (the one I run into) is there is a problem with the Comment.User, the Comment.UserId does not get saved automatically\n\t\tfor i := 0; i < len(addresses); i++ {\n\t\t\taddress := addresses[i]\n\t\t\tfor j := 0; j < len(users); j++ {\n\t\t\t\tuser := users[j]\n\t\t\t\tif address.UserId == user.ID {\n\t\t\t\t\taddresses[i].User = users[j]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn addresses, totalAddressesCount\n}\n\nfunc FetchAddress(addressId uint) (address models.Address) {\n\tdatabase := infrastructure.GetDb()\n\tdatabase.First(&address, addressId)\n\treturn address\n}\n\nfunc FetchIdsFromAddress(addressId uint) (address models.Address) {\n\tdatabase := infrastructure.GetDb()\n\tdatabase.Select(\"id, user_id\").First(&address, addressId)\n\treturn\n}\n"
  },
  {
    "path": "services/categories.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchAllCategories() ([]models.Category, error) {\n\tdatabase := infrastructure.GetDb()\n\tvar categories []models.Category\n\terr := database.Preload(\"Images\", \"category_id IS NOT NULL\").Find(&categories).Error\n\treturn categories, err\n}\n"
  },
  {
    "path": "services/comments.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchCommentsPage(productId, page int, page_size int) ([]models.Comment, int) {\n\t// TODO: Why Preload does not load the User? the error is can't preload field User for models.Comment\n\n\tvar comments []models.Comment\n\tvar totalCommentCount int\n\tdatabase := infrastructure.GetDb()\n\tdatabase.Model(&comments).Where(&models.Comment{ProductId: uint(productId)}).Count(&totalCommentCount)\n\tdatabase.Where(&models.Comment{ProductId: uint(productId)}).\n\t\tOffset((page - 1) * page_size).Limit(page_size).\n\t\tPreload(\"User\").\n\t\tFind(&comments)\n\n\t// `Where in` using other columns different than ID\n\t// database.Where(\"username in (?)\", []string{\"admin\", \"melardev\"}).Find(&users)\n\tvar userIds = make([]uint, len(comments))\n\tvar users []models.User\n\tfor i := 0; i < len(comments); i++ {\n\t\tuserIds[i] = comments[i].UserId\n\t}\n\tdatabase.Select(\"id, username\").Where(userIds).Find(&users)\n\n\t// If the user gets deleted and the comment is still in the database we may have less users than comments\n\t// Another scenario (the one I run into) is there is a problem with the Comment.User, the Comment.UserId does not get saved automatically\n\tfor i := 0; i < len(comments); i++ {\n\t\tcomment := comments[i]\n\t\tfor j := 0; j < len(users); j++ {\n\t\t\tuser := users[j]\n\t\t\tif comment.UserId == user.ID {\n\t\t\t\tcomments[i].User = users[j]\n\t\t\t}\n\t\t}\n\n\t}\n\treturn comments, totalCommentCount\n}\n\nfunc FetchCommentById(id int, includes ...bool) models.Comment {\n\tincludeUser := false\n\tif len(includes) > 0 {\n\t\tincludeUser = includes[0]\n\t}\n\tincludeProduct := false\n\tif len(includes) > 1 {\n\t\tincludeProduct = includes[1]\n\t}\n\tdatabase := infrastructure.GetDb()\n\tvar comment models.Comment\n\tif includeProduct && includeUser {\n\t\tdatabase.Preload(\"User\").Preload(\"Product\").Find(&comment, id)\n\t} else if includeUser {\n\t\tdatabase.Preload(\"User\").Find(&comment, id)\n\t} else if includeProduct {\n\t\tdatabase.Preload(\"Product\").Find(&comment, id)\n\t} else {\n\t\tdatabase.Find(&comment, id)\n\t}\n\treturn comment\n}\n\nfunc DeleteComment(condition interface{}) error {\n\tdatabase := infrastructure.GetDb()\n\terr := database.Where(condition).Delete(models.Comment{}).Error\n\treturn err\n}\n"
  },
  {
    "path": "services/orders.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchOrdersPage(userId uint, page, pageSize int) (orders []models.Order, totalOrdersCount int, err error) {\n\tdatabase := infrastructure.GetDb()\n\n\ttotalOrdersCount = 0\n\n\tquery := database.Model(&models.Order{}).Where(&models.Order{UserId: userId})\n\tquery.Count(&totalOrdersCount)\n\n\terr = query.Offset((page - 1) * pageSize).Limit(pageSize).\n\t\t// TODO: Why Preload(\"Address\") does not work?, perhaps OrderItems does\n\t\t// Preload(\"OrderItems\").Preload(\"Address\").\n\t\tFind(&orders).Error\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar orderIds = make([]uint, len(orders))\n\tfor i := 0; i < len(orders); i++ {\n\t\torderIds[i] = orders[i].ID\n\t}\n\n\tvar orderItems []models.OrderItem\n\tif len(orders) > 0 {\n\t\t//\n\t\tdatabase.Select(\"id, order_id\").Where(\"order_id in (?)\", orderIds).Find(&orderItems)\n\n\t\tfor i := 0; i < len(orderItems); i++ {\n\t\t\toi := orderItems[i]\n\t\t\tfor j := 0; j < len(orders); j++ {\n\t\t\t\tif oi.OrderId == orders[j].ID {\n\t\t\t\t\torders[j].OrderItemsCount = orders[j].OrderItemsCount + 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn orders, totalOrdersCount, err\n}\n\nfunc FetchOrderDetails(orderId uint) (order models.Order, err error) {\n\tdatabase := infrastructure.GetDb()\n\terr = database.Model(models.Order{}).Preload(\"OrderItems\").First(&order, orderId).Error\n\tvar address models.Address\n\tdatabase.Model(&order).Related(&address)\n\torder.Address = address\n\treturn order, err\n}\n"
  },
  {
    "path": "services/products.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchProductsPage(page int, page_size int) ([]models.Product, int, []int, error) {\n\tdatabase := infrastructure.GetDb()\n\tvar products []models.Product\n\tvar count int\n\ttx := database.Begin()\n\tdatabase.Model(&products).Count(&count)\n\tdatabase.Offset((page - 1) * page_size).Limit(page_size).Find(&products)\n\ttx.Model(&products).\n\t\tPreload(\"Tags\").Preload(\"Categories\").Preload(\"Images\").\n\t\tOrder(\"created_at desc\").Offset((page - 1) * page_size).Limit(page_size).Find(&products)\n\tcommentsCount := make([]int, len(products))\n\n\tfor index, product := range products {\n\t\tcommentsCount[index] = tx.Model(&product).Association(\"Comments\").Count()\n\t}\n\terr := tx.Commit().Error\n\treturn products, count, commentsCount, err\n}\n\nfunc FetchProductDetails(condition interface{}, optional ...bool) models.Product {\n\tdatabase := infrastructure.GetDb()\n\tvar product models.Product\n\n\tquery := database.Where(condition).\n\t\tPreload(\"Tags\").Preload(\"Categories\").Preload(\"Images\").Preload(\"Comments\")\n\t// Unfortunately .Preload(\"Comments.User\") does not work as the doc states ...\n\tquery.First(&product)\n\tincludeUserComment := false\n\n\tif len(optional) > 0 {\n\t\tincludeUserComment = optional[0]\n\t}\n\n\tif includeUserComment {\n\n\t\tfor i := 0; i < len(product.Comments); i++ {\n\t\t\tdatabase.Model(&product.Comments[i]).Related(&product.Comments[i].User, \"UserId\")\n\t\t}\n\n\t\tvar userIds = make([]uint, len(product.Comments))\n\t\tvar users []models.User\n\t\tfor i := 0; i < len(product.Comments); i++ {\n\t\t\tuserIds[i] = product.Comments[i].UserId\n\t\t}\n\t\t// WHERE users.id IN userIds; This will also work: Select([]string{\"id\", \"username\"})\n\t\tdatabase.Select(\"id, username\").Where(userIds).Find(&users)\n\n\t\tfor i := 0; i < len(product.Comments); i++ {\n\t\t\tuser := users[i]\n\t\t\tcomment := product.Comments[i]\n\t\t\tif comment.UserId == user.ID {\n\t\t\t\tproduct.Comments[i].User = users[i]\n\t\t\t}\n\t\t}\n\t}\n\n\treturn product\n}\n\nfunc FetchProductId(slug string) (uint, error) {\n\tproductId := -1\n\tdatabase := infrastructure.GetDb()\n\terr := database.Model(&models.Product{}).Where(&models.Product{Slug: slug}).Select(\"id\").Row().Scan(&productId)\n\treturn uint(productId), err\n}\n\nfunc SetTags(product *models.Product, tags []string) error {\n\tdatabase := infrastructure.GetDb()\n\tvar tagList []models.Tag\n\tfor _, tag := range tags {\n\t\tvar tagModel models.Tag\n\t\terr := database.FirstOrCreate(&tagModel, models.Tag{Name: tag}).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttagList = append(tagList, tagModel)\n\t}\n\tproduct.Tags = tagList\n\treturn nil\n}\n\nfunc Update(product *models.Product, data interface{}) error {\n\tdatabase := infrastructure.GetDb()\n\terr := database.Model(product).Update(data).Error\n\treturn err\n}\n\nfunc DeleteProduct(condition interface{}) error {\n\tdb := infrastructure.GetDb()\n\terr := db.Where(condition).Delete(models.Product{}).Error\n\treturn err\n}\n\nfunc FetchProductsIdNameAndPrice(productIds []uint) (products []models.Product, err error) {\n\tdatabase := infrastructure.GetDb()\n\terr = database.Select([]string{\"id\", \"name\", \"slug\", \"price\"}).Find(&products, productIds).Error\n\treturn products, err\n}\n"
  },
  {
    "path": "services/shared.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n)\n\nfunc CreateOne(data interface{}) error {\n\tdatabase := infrastructure.GetDb()\n\terr := database.Create(data).Error\n\treturn err\n}\n\nfunc SaveOne(data interface{}) error {\n\tdatabase := infrastructure.GetDb()\n\terr := database.Save(data).Error\n\treturn err\n}\n"
  },
  {
    "path": "services/tags.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\nfunc FetchAllTags() ([]models.Tag, error) {\n\tdatabase := infrastructure.GetDb()\n\tvar tags []models.Tag\n\terr := database.Preload(\"Images\", \"tag_id IS NOT NULL\").Find(&tags).Error\n\treturn tags, err\n}\n"
  },
  {
    "path": "services/users.go",
    "content": "package services\n\nimport (\n\t\"github.com/melardev/GoGonicEcommerceApi/infrastructure\"\n\t\"github.com/melardev/GoGonicEcommerceApi/models\"\n)\n\n// You could input the conditions and it will return an User in database with error info.\n// \tuserModel, err := FindOneUser(&User{Username: \"username0\"})\nfunc FindOneUser(condition interface{}) (models.User, error) {\n\tdatabase := infrastructure.GetDb()\n\tvar user models.User\n\n\terr := database.Where(condition).Preload(\"Roles\").First(&user).Error\n\treturn user, err\n}\n\n// You could update properties of an User to database returning with error info.\n//  err := db.Model(userModel).Update(User{Username: \"wangzitian0\"}).Error\nfunc UpdateUser(user models.User, data interface{}) error {\n\tdatabase := infrastructure.GetDb()\n\terr := database.Model(user).Update(data).Error\n\treturn err\n}\n"
  }
]