You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-07-17 01:42:36 +02:00
Merge branch 'master' into apply-dependency-injection
This commit is contained in:
15
README.md
15
README.md
@ -3,6 +3,7 @@
|
|||||||
Copyright 2019, Geeks Accelerator
|
Copyright 2019, Geeks Accelerator
|
||||||
twins@geeksaccelerator.com
|
twins@geeksaccelerator.com
|
||||||
|
|
||||||
|
Sponsored by Copper Valley Telecom
|
||||||
|
|
||||||
The SaaS Starter Kit is a set of libraries for building scalable software-as-a-service (SaaS) applications that helps
|
The SaaS Starter Kit is a set of libraries for building scalable software-as-a-service (SaaS) applications that helps
|
||||||
preventing both misuse and fraud. The goal of this project is to provide a proven starting point for new
|
preventing both misuse and fraud. The goal of this project is to provide a proven starting point for new
|
||||||
@ -62,7 +63,7 @@ delivered to clients.
|
|||||||
a knowledge of a completely different expertise - DevOps. This project provides a complete continuous build pipeline that
|
a knowledge of a completely different expertise - DevOps. This project provides a complete continuous build pipeline that
|
||||||
will push the code to production with minimal effort using serverless deployments to AWS Fargate with GitLab CI/CD.
|
will push the code to production with minimal effort using serverless deployments to AWS Fargate with GitLab CI/CD.
|
||||||
5. Observability - Ensure the code is running as expected in a remote environment. This project implements Datadog to
|
5. Observability - Ensure the code is running as expected in a remote environment. This project implements Datadog to
|
||||||
facilitate exposing metrics, logs and request tracing to obversabe and validate your services are stable and responsive
|
facilitate exposing metrics, logs and request tracing to obverse and validate your services are stable and responsive
|
||||||
for your clients (hopefully paying clients).
|
for your clients (hopefully paying clients).
|
||||||
|
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ facilitate exposing metrics, logs and request tracing to obversabe and validate
|
|||||||
The example project is a complete starter kit for building SasS with GoLang. It provides two example services:
|
The example project is a complete starter kit for building SasS with GoLang. It provides two example services:
|
||||||
* Web App - Responsive web application to provide service to clients. Includes user signup and user authentication for
|
* Web App - Responsive web application to provide service to clients. Includes user signup and user authentication for
|
||||||
direct client interaction via their web browsers.
|
direct client interaction via their web browsers.
|
||||||
* Web API - REST API with JWT authentication that renders results as JSON. This allows clients and other third-pary companies to develop deep
|
* Web API - REST API with JWT authentication that renders results as JSON. This allows clients and other third-party companies to develop deep
|
||||||
integrations with the project.
|
integrations with the project.
|
||||||
|
|
||||||
The example project also provides these tools:
|
The example project also provides these tools:
|
||||||
@ -106,7 +107,7 @@ Accordingly, the project architecture is illustrated with the following diagram.
|
|||||||
With SaaS, a client subscribes to an online service you provide them. The example project provides functionality for
|
With SaaS, a client subscribes to an online service you provide them. The example project provides functionality for
|
||||||
clients to subscribe and then once subscribed they can interact with your software service.
|
clients to subscribe and then once subscribed they can interact with your software service.
|
||||||
|
|
||||||
The initial contributors to this project are building this saas-starter-kit based on their years of experience building enterprise B2B SaaS. Particularily, this saas-starter-kit is based on their most recent experience building the
|
The initial contributors to this project are building this saas-starter-kit based on their years of experience building enterprise B2B SaaS. Particularly, this saas-starter-kit is based on their most recent experience building the
|
||||||
B2B SaaS for [standard operating procedure software](https://keeni.space) (written entirely in Golang). Please refer to the Keeni.Space website,
|
B2B SaaS for [standard operating procedure software](https://keeni.space) (written entirely in Golang). Please refer to the Keeni.Space website,
|
||||||
its [SOP software pricing](https://keeni.space/pricing) and its signup process. The SaaS web app is then available at
|
its [SOP software pricing](https://keeni.space/pricing) and its signup process. The SaaS web app is then available at
|
||||||
[app.keeni.space](https://app.keeni.space). They plan on leveraging this experience and build it into a simplified set
|
[app.keeni.space](https://app.keeni.space). They plan on leveraging this experience and build it into a simplified set
|
||||||
@ -175,7 +176,7 @@ $ git clone git@gitlab.com:geeks-accelerator/oss/saas-starter-kit.git
|
|||||||
$ cd saas-starter-kit/
|
$ cd saas-starter-kit/
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have Go Modules enabled, you should be able compile the project locally. If you have Go Modulels disabled, see
|
If you have Go Modules enabled, you should be able compile the project locally. If you have Go Modules disabled, see
|
||||||
the next section.
|
the next section.
|
||||||
|
|
||||||
|
|
||||||
@ -386,7 +387,7 @@ Policy Document: {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a new user with programic access and directly attach it the policy `SaasStarterKitDevServices`
|
Create a new user with programmatic access and directly attach it the policy `SaasStarterKitDevServices`
|
||||||
|
|
||||||
4. Create a new docker-compose config file
|
4. Create a new docker-compose config file
|
||||||
```bash
|
```bash
|
||||||
@ -395,7 +396,7 @@ Create a new user with programic access and directly attach it the policy `SaasS
|
|||||||
|
|
||||||
5. Update .env_docker_compose with the Access key ID and Secret access key
|
5. Update .env_docker_compose with the Access key ID and Secret access key
|
||||||
|
|
||||||
6. Update `.gitlab-ci.yml` with relevent details.
|
6. Update `.gitlab-ci.yml` with relevant details.
|
||||||
|
|
||||||
|
|
||||||
### Optional. Set AWS and Datadog Configs
|
### Optional. Set AWS and Datadog Configs
|
||||||
@ -489,7 +490,7 @@ For more details on this service, read [web-app readme](https://gitlab.com/geeks
|
|||||||
Schema is a minimalistic database migration helper that can manually be invoked via CLI. It provides schema versioning
|
Schema is a minimalistic database migration helper that can manually be invoked via CLI. It provides schema versioning
|
||||||
and migration rollback.
|
and migration rollback.
|
||||||
|
|
||||||
To support POD architecture, the schema for the entire project is defined globally and is located inside internal:
|
The schema for the entire project is defined globally and is located inside internal:
|
||||||
[internal/schema](https://gitlab.com/geeks-accelerator/oss/saas-starter-kit/tree/master/internal/schema)
|
[internal/schema](https://gitlab.com/geeks-accelerator/oss/saas-starter-kit/tree/master/internal/schema)
|
||||||
|
|
||||||
Keeping a global schema helps ensure business logic can be decoupled across multiple packages. It is a firm belief that
|
Keeping a global schema helps ensure business logic can be decoupled across multiple packages. It is a firm belief that
|
||||||
|
@ -254,7 +254,7 @@ swag init
|
|||||||
|
|
||||||
### Additional Swagger Annotations
|
### Additional Swagger Annotations
|
||||||
|
|
||||||
Below are some additional example annotions that can be added to `main.go`
|
Below are some additional example annotations that can be added to `main.go`
|
||||||
```go
|
```go
|
||||||
// @title SaaS Example API
|
// @title SaaS Example API
|
||||||
// @description This provides a public API...
|
// @description This provides a public API...
|
||||||
|
@ -24,7 +24,7 @@ http://127.0.0.1:3000/
|
|||||||
|
|
||||||
While the web-api service has
|
While the web-api service has
|
||||||
significant functionality, this web-app service is still in development. Currently this web-app services only resizes
|
significant functionality, this web-app service is still in development. Currently this web-app services only resizes
|
||||||
an image and displays resvised versions of it on the index page. See section below on Future Functionality.
|
an image and displays resized versions of it on the index page. See section below on Future Functionality.
|
||||||
|
|
||||||
If you would like to help, please email twins@geeksinthewoods.com.
|
If you would like to help, please email twins@geeksinthewoods.com.
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ type GeoRepository interface {
|
|||||||
FindCountries(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.Country, error)
|
FindCountries(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.Country, error)
|
||||||
FindCountryTimezones(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.CountryTimezone, error)
|
FindCountryTimezones(ctx context.Context, orderBy, where string, args ...interface{}) ([]*geonames.CountryTimezone, error)
|
||||||
ListTimezones(ctx context.Context) ([]string, error)
|
ListTimezones(ctx context.Context) ([]string, error)
|
||||||
LoadGeonames(ctx context.Context, rr chan<- interface{}, countries ...string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeonameByPostalCode...
|
// GeonameByPostalCode...
|
||||||
|
@ -74,7 +74,7 @@ func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
var v datatable.ColumnValue
|
var v datatable.ColumnValue
|
||||||
switch col.Field {
|
switch col.Field {
|
||||||
case "id":
|
case "id":
|
||||||
v.Value = fmt.Sprintf("%d", q.ID)
|
v.Value = fmt.Sprintf("%s", q.ID)
|
||||||
case "name":
|
case "name":
|
||||||
v.Value = q.Name
|
v.Value = q.Name
|
||||||
v.Formatted = fmt.Sprintf("<a href='%s'>%s</a>", urlProjectsView(q.ID), v.Value)
|
v.Formatted = fmt.Sprintf("<a href='%s'>%s</a>", urlProjectsView(q.ID), v.Value)
|
||||||
|
@ -102,7 +102,7 @@ func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
var v datatable.ColumnValue
|
var v datatable.ColumnValue
|
||||||
switch col.Field {
|
switch col.Field {
|
||||||
case "id":
|
case "id":
|
||||||
v.Value = fmt.Sprintf("%d", q.ID)
|
v.Value = fmt.Sprintf("%s", q.ID)
|
||||||
case "name":
|
case "name":
|
||||||
if strings.TrimSpace(q.Name) == "" {
|
if strings.TrimSpace(q.Name) == "" {
|
||||||
v.Value = q.Email
|
v.Value = q.Email
|
||||||
|
@ -5,11 +5,16 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
|
|
||||||
@ -189,53 +194,72 @@ func (repo *Repository) FindGeonameRegions(ctx context.Context, orderBy, where s
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadGeonames enables streaming retrieval of GeoNames. The downloaded results
|
// GetGeonameCountry downloads geoname data for the country.
|
||||||
// will be written to the interface{} resultReceiver channel enabling processing the results while
|
// Parses data and returns slice of Geoname
|
||||||
// they're still being fetched. After all pages have been processed the channel is closed.
|
func (repo *Repository) GetGeonameCountry(ctx context.Context, country string) ([]Geoname, error) {
|
||||||
// Possible types sent to the channel are limited to:
|
res := make([]Geoname, 0)
|
||||||
// - error
|
var err error
|
||||||
// - GeoName
|
var resp *http.Response
|
||||||
func (repo *Repository) LoadGeonames(ctx context.Context, rr chan<- interface{}, countries ...string) {
|
|
||||||
defer close(rr)
|
|
||||||
|
|
||||||
if len(countries) == 0 {
|
|
||||||
countries = ValidGeonameCountries(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, country := range countries {
|
|
||||||
loadGeonameCountry(ctx, rr, country)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadGeonameCountry enables streaming retrieval of GeoNames. The downloaded results
|
|
||||||
// will be written to the interface{} resultReceiver channel enabling processing the results while
|
|
||||||
// they're still being fetched.
|
|
||||||
// Possible types sent to the channel are limited to:
|
|
||||||
// - error
|
|
||||||
// - GeoName
|
|
||||||
func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country string) {
|
|
||||||
u := fmt.Sprintf("http://download.geonames.org/export/zip/%s.zip", country)
|
u := fmt.Sprintf("http://download.geonames.org/export/zip/%s.zip", country)
|
||||||
resp, err := pester.Get(u)
|
|
||||||
if err != nil {
|
|
||||||
rr <- errors.WithMessagef(err, "Failed to read countries from '%s'", u)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
br := bufio.NewReader(resp.Body)
|
h := fmt.Sprintf("%x", md5.Sum([]byte(u)))
|
||||||
|
cp := filepath.Join(os.TempDir(), h+".zip")
|
||||||
|
|
||||||
|
if _, err := os.Stat(cp); err != nil {
|
||||||
|
resp, err = pester.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
// Add re-try three times after failing first time
|
||||||
|
// This reduces the risk when network is lagy, we still have chance to re-try.
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
resp, err = pester.Get(u)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithMessagef(err, "Failed to read countries from '%s'", u)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Create the file
|
||||||
|
out, err := os.Create(cp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// Write the body to file
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(cp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
br := bufio.NewReader(f)
|
||||||
|
|
||||||
buff := bytes.NewBuffer([]byte{})
|
buff := bytes.NewBuffer([]byte{})
|
||||||
size, err := io.Copy(buff, br)
|
size, err := io.Copy(buff, br)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
return
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b := bytes.NewReader(buff.Bytes())
|
b := bytes.NewReader(buff.Bytes())
|
||||||
zr, err := zip.NewReader(b, size)
|
zr, err := zip.NewReader(b, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
return
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range zr.File {
|
for _, f := range zr.File {
|
||||||
@ -245,8 +269,8 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
|
|||||||
|
|
||||||
fh, err := f.Open()
|
fh, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
return
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(fh)
|
scanner := bufio.NewScanner(fh)
|
||||||
@ -264,27 +288,12 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
|
|||||||
|
|
||||||
lines, err := r.ReadAll()
|
lines, err := r.ReadAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range lines {
|
for _, row := range lines {
|
||||||
|
|
||||||
/*
|
|
||||||
fmt.Println("CountryCode: row[0]", row[0])
|
|
||||||
fmt.Println("PostalCode: row[1]", row[1])
|
|
||||||
fmt.Println("PlaceName: row[2]", row[2])
|
|
||||||
fmt.Println("StateName: row[3]", row[3])
|
|
||||||
fmt.Println("StateCode : row[4]", row[4])
|
|
||||||
fmt.Println("CountyName: row[5]", row[5])
|
|
||||||
fmt.Println("CountyCode : row[6]", row[6])
|
|
||||||
fmt.Println("CommunityName: row[7]", row[7])
|
|
||||||
fmt.Println("CommunityCode: row[8]", row[8])
|
|
||||||
fmt.Println("Latitude: row[9]", row[9])
|
|
||||||
fmt.Println("Longitude: row[10]", row[10])
|
|
||||||
fmt.Println("Accuracy: row[11]", row[11])
|
|
||||||
*/
|
|
||||||
|
|
||||||
gn := Geoname{
|
gn := Geoname{
|
||||||
CountryCode: row[0],
|
CountryCode: row[0],
|
||||||
PostalCode: row[1],
|
PostalCode: row[1],
|
||||||
@ -299,30 +308,32 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
|
|||||||
if row[9] != "" {
|
if row[9] != "" {
|
||||||
gn.Latitude, err = decimal.NewFromString(row[9])
|
gn.Latitude, err = decimal.NewFromString(row[9])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if row[10] != "" {
|
if row[10] != "" {
|
||||||
gn.Longitude, err = decimal.NewFromString(row[10])
|
gn.Longitude, err = decimal.NewFromString(row[10])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if row[11] != "" {
|
if row[11] != "" {
|
||||||
gn.Accuracy, err = strconv.Atoi(row[11])
|
gn.Accuracy, err = strconv.Atoi(row[11])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rr <- gn
|
res = append(res, gn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
rr <- errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -135,7 +136,7 @@ func main() {
|
|||||||
func API(shutdown chan os.Signal, log *log.Logger) http.Handler {
|
func API(shutdown chan os.Signal, log *log.Logger) http.Handler {
|
||||||
|
|
||||||
// Construct the web.App which holds all routes as well as common Middleware.
|
// Construct the web.App which holds all routes as well as common Middleware.
|
||||||
app := web.NewApp(shutdown, log, mid.Trace(), mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
|
app := web.NewApp(shutdown, log, webcontext.Env_Dev, mid.Logger(log))
|
||||||
|
|
||||||
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler)
|
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler)
|
||||||
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler)
|
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
_ "geeks-accelerator/oss/saas-starter-kit/internal/mid/saas-swagger/example/docs"
|
_ "geeks-accelerator/oss/saas-starter-kit/internal/mid/saas-swagger/example/docs"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ func TestWrapHandler(t *testing.T) {
|
|||||||
log := log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
log := log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||||
log.SetOutput(ioutil.Discard)
|
log.SetOutput(ioutil.Discard)
|
||||||
|
|
||||||
app := web.NewApp(nil, log)
|
app := web.NewApp(nil, log, webcontext.Env_Dev)
|
||||||
app.Handle("GET", "/swagger/*", WrapHandler)
|
app.Handle("GET", "/swagger/*", WrapHandler)
|
||||||
|
|
||||||
w1 := performRequest("GET", "/swagger/index.html", app)
|
w1 := performRequest("GET", "/swagger/index.html", app)
|
||||||
|
@ -3,12 +3,13 @@ package logger
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithContext manual injects context values to log message including Trace ID
|
// WithContext manual injects context values to log message including Trace ID
|
||||||
func WithContext(ctx context.Context, msg string) string {
|
func WithContext(ctx context.Context, msg string) string {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(webcontext.KeyValues).(*webcontext.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,10 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||||
|
|
||||||
"github.com/geeks-accelerator/sqlxmigrate"
|
"github.com/geeks-accelerator/sqlxmigrate"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
// migration already exists in the migrations table it will be skipped.
|
// migration already exists in the migrations table it will be skipped.
|
||||||
func migrationList(ctx context.Context, db *sqlx.DB, log *log.Logger, isUnittest bool) []*sqlxmigrate.Migration {
|
func migrationList(ctx context.Context, db *sqlx.DB, log *log.Logger, isUnittest bool) []*sqlxmigrate.Migration {
|
||||||
geoRepo := geonames.NewRepository(db)
|
geoRepo := geonames.NewRepository(db)
|
||||||
|
|
||||||
return []*sqlxmigrate.Migration{
|
return []*sqlxmigrate.Migration{
|
||||||
// Create table users.
|
// Create table users.
|
||||||
{
|
{
|
||||||
@ -215,7 +217,7 @@ func migrationList(ctx context.Context, db *sqlx.DB, log *log.Logger, isUnittest
|
|||||||
},
|
},
|
||||||
// Load new geonames table.
|
// Load new geonames table.
|
||||||
{
|
{
|
||||||
ID: "20190731-02h",
|
ID: "20190731-02l",
|
||||||
Migrate: func(tx *sql.Tx) error {
|
Migrate: func(tx *sql.Tx) error {
|
||||||
|
|
||||||
schemas := []string{
|
schemas := []string{
|
||||||
@ -242,33 +244,91 @@ func migrationList(ctx context.Context, db *sqlx.DB, log *log.Logger, isUnittest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
q := "insert into geonames " +
|
countries := geonames.ValidGeonameCountries(ctx)
|
||||||
"(country_code,postal_code,place_name,state_name,state_code,county_name,county_code,community_name,community_code,latitude,longitude,accuracy) " +
|
if isUnittest {
|
||||||
"values(?,?,?,?,?,?,?,?,?,?,?,?)"
|
countries = []string{"US"}
|
||||||
q = db.Rebind(q)
|
|
||||||
stmt, err := db.Prepare(q)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessagef(err, "Failed to prepare sql query '%s'", q)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isUnittest {
|
ncol := 12
|
||||||
|
fn := func(geoNames []geonames.Geoname) error {
|
||||||
|
valueStrings := make([]string, 0, len(geoNames))
|
||||||
|
valueArgs := make([]interface{}, 0, len(geoNames)*ncol)
|
||||||
|
for _, geoname := range geoNames {
|
||||||
|
valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
|
|
||||||
} else {
|
valueArgs = append(valueArgs, geoname.CountryCode)
|
||||||
resChan := make(chan interface{})
|
valueArgs = append(valueArgs, geoname.PostalCode)
|
||||||
go geoRepo.LoadGeonames(ctx, resChan)
|
valueArgs = append(valueArgs, geoname.PlaceName)
|
||||||
|
|
||||||
for r := range resChan {
|
valueArgs = append(valueArgs, geoname.StateName)
|
||||||
switch v := r.(type) {
|
valueArgs = append(valueArgs, geoname.StateCode)
|
||||||
case geonames.Geoname:
|
valueArgs = append(valueArgs, geoname.CountyName)
|
||||||
_, err = stmt.Exec(v.CountryCode, v.PostalCode, v.PlaceName, v.StateName, v.StateCode, v.CountyName, v.CountyCode, v.CommunityName, v.CommunityCode, v.Latitude, v.Longitude, v.Accuracy)
|
|
||||||
|
valueArgs = append(valueArgs, geoname.CountyCode)
|
||||||
|
valueArgs = append(valueArgs, geoname.CommunityName)
|
||||||
|
valueArgs = append(valueArgs, geoname.CommunityCode)
|
||||||
|
|
||||||
|
valueArgs = append(valueArgs, geoname.Latitude)
|
||||||
|
valueArgs = append(valueArgs, geoname.Longitude)
|
||||||
|
valueArgs = append(valueArgs, geoname.Accuracy)
|
||||||
|
}
|
||||||
|
insertStmt := fmt.Sprintf("insert into geonames "+
|
||||||
|
"(country_code,postal_code,place_name,state_name,state_code,county_name,county_code,community_name,community_code,latitude,longitude,accuracy) "+
|
||||||
|
"VALUES %s", strings.Join(valueStrings, ","))
|
||||||
|
insertStmt = db.Rebind(insertStmt)
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(insertStmt)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessagef(err, "Failed to prepare sql query '%s'", insertStmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stmt.Exec(valueArgs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
for _, country := range countries {
|
||||||
|
//fmt.Println("LoadGeonames: start country: ", country)
|
||||||
|
v, err := geoRepo.GetGeonameCountry(context.Background(), country)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
//fmt.Println("Geoname records: ", len(v))
|
||||||
|
// Max argument values of Postgres is about 54460. So the batch size for bulk insert is selected 4500*12 (ncol)
|
||||||
|
batch := 4500
|
||||||
|
n := len(v) / batch
|
||||||
|
|
||||||
|
//fmt.Println("Number of batch: ", n)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
err := fn(v)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
vn := v[i*batch : (i+1)*batch]
|
||||||
|
err := fn(vn)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
if n > 0 && n%25 == 0 {
|
||||||
|
time.Sleep(200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(v)%batch > 0 {
|
||||||
|
fmt.Println("Remain part: ", len(v)-n*batch)
|
||||||
|
vn := v[n*batch:]
|
||||||
|
err := fn(vn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
case error:
|
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//fmt.Println("Insert Geoname took: ", time.Since(start))
|
||||||
|
//fmt.Println("LoadGeonames: end country: ", country)
|
||||||
}
|
}
|
||||||
|
log.Println("Total Geonames population took: ", time.Since(start))
|
||||||
|
|
||||||
queries := []string{
|
queries := []string{
|
||||||
`create index idx_geonames_country_code on geonames (country_code)`,
|
`create index idx_geonames_country_code on geonames (country_code)`,
|
||||||
|
@ -26,7 +26,7 @@ in other configuration files. And since this project is open-source, we wanted t
|
|||||||
|
|
||||||
If you don't have an AWS account, signup for one now and then proceed with the deployment setup.
|
If you don't have an AWS account, signup for one now and then proceed with the deployment setup.
|
||||||
|
|
||||||
We assume that if you are deploying the SaaS Stater Kit, you are starting from scratch with no existing dependencies.
|
We assume that if you are deploying the SaaS Starter Kit, you are starting from scratch with no existing dependencies.
|
||||||
This however, excludes any domain names that you would like to use for resolving your services publicly. To use any
|
This however, excludes any domain names that you would like to use for resolving your services publicly. To use any
|
||||||
pre-purchased domain names, make sure they are added to Route 53 in the AWS account. Or you can let the deploy script
|
pre-purchased domain names, make sure they are added to Route 53 in the AWS account. Or you can let the deploy script
|
||||||
create a new zone is Route 53 and update the DNS for the domain name when your ready to make the transition. It is
|
create a new zone is Route 53 and update the DNS for the domain name when your ready to make the transition. It is
|
||||||
|
Reference in New Issue
Block a user