Skip to content

Commit 793e4fb

Browse files
committed
chore(golang): implement logic for accesing database for get and get_list
Add dependency on jmoiron/sqlx for named parameters support (and to simplify result extraction into structs). See for details: go-sql-driver/mysql#561 (comment) See also: - http://go-database-sql.org - https://github.com/jmoiron/sqlx - http://jmoiron.github.io/sqlx/ Part of #9
1 parent 7cacd68 commit 793e4fb

File tree

7 files changed

+148
-71
lines changed

7 files changed

+148
-71
lines changed

examples/go/app.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package main
22

3-
import "database/sql"
43
import "fmt"
54
import "net/http"
65
import "os"
76
import "github.com/go-chi/chi"
7+
import "github.com/jmoiron/sqlx"
88

99
import _ "github.com/go-sql-driver/mysql"
1010

@@ -26,9 +26,9 @@ func main() {
2626
}
2727

2828
dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper)
29-
db, err := sql.Open("mysql", dsn)
29+
db, err := sqlx.Open("mysql", dsn)
3030
if err != nil {
31-
fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err)
31+
fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err)
3232
os.Exit(1)
3333
}
3434
defer db.Close()
@@ -39,7 +39,7 @@ func main() {
3939
}
4040

4141
r := chi.NewRouter()
42-
registerRoutes(r)
42+
registerRoutes(r, db)
4343

4444
port := os.Getenv("PORT")
4545
if port == "" {

examples/go/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ go 1.14
55
require (
66
github.com/go-chi/chi v4.1.2+incompatible
77
github.com/go-sql-driver/mysql v1.5.0
8+
github.com/jmoiron/sqlx v1.2.0
89
)

examples/go/routes.go

+79-33
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,96 @@
11
package main
22

3+
import "database/sql"
34
import "encoding/json"
45
import "fmt"
56
import "net/http"
7+
import "os"
68
import "strconv"
79
import "github.com/go-chi/chi"
10+
import "github.com/jmoiron/sqlx"
811

912
type Category struct {
10-
Id int `json:"id"`
11-
Name *string `json:"name"`
12-
NameRu *string `json:"name_ru"`
13-
Slug *string `json:"slug"`
13+
Id int `json:"id" db:"id"`
14+
Name string `json:"name" db:"name"`
15+
NameRu *string `json:"name_ru" db:"name_ru"`
16+
Slug string `json:"slug" db:"slug"`
1417
}
1518

1619
type Dto1 struct {
17-
Counter string `json:"counter"`
20+
Counter *string `json:"counter,omitempty" db:"counter"`
1821
}
1922

2023
type Dto3 struct {
21-
Id string `json:"id"`
22-
Name string `json:"name"`
23-
NameRu string `json:"nameRu"`
24-
Slug string `json:"slug"`
24+
Id *string `json:"id,omitempty" db:"id"`
25+
Name *string `json:"name,omitempty" db:"name"`
26+
NameRu *string `json:"name_ru,omitempty" db:"name_ru"`
27+
Slug *string `json:"slug,omitempty" db:"slug"`
2528
}
2629

2730
type Dto4 struct {
28-
Name string `json:"name"`
29-
NameRu string `json:"nameRu"`
30-
Slug string `json:"slug"`
31-
UserId string `json:"userId"`
31+
Name *string `json:"name,omitempty" db:"name"`
32+
NameRu *string `json:"name_ru,omitempty" db:"name_ru"`
33+
Slug *string `json:"slug,omitempty" db:"slug"`
34+
UserId *string `json:"user_id,omitempty" db:"user_id"`
3235
}
3336

34-
func registerRoutes(r chi.Router) {
37+
func registerRoutes(r chi.Router, db *sqlx.DB) {
3538
categories := make(map[int]Category)
3639
cnt := 0
3740

3841
r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) {
39-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
40-
fmt.Fprintf(w, `{"counter": %d}`, len(categories))
41-
42+
var result Dto1
43+
err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories")
44+
switch err {
45+
case sql.ErrNoRows:
46+
w.WriteHeader(http.StatusNotFound)
47+
case nil:
48+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
49+
json.NewEncoder(w).Encode(&result)
50+
default:
51+
fmt.Fprintf(os.Stderr, "Get failed: %v\n", err)
52+
w.WriteHeader(http.StatusInternalServerError)
53+
}
4254
})
4355

4456
r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) {
45-
id, _ := strconv.Atoi(chi.URLParam(r, "categoryId"))
46-
category, exist := categories[id]
47-
if !exist {
48-
w.WriteHeader(http.StatusNotFound)
57+
nstmt, err := db.PrepareNamed("SELECT COUNT(DISTINCT s.category_id) AS counter FROM collections_series cs JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :collectionId")
58+
if err != nil {
59+
fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err)
60+
w.WriteHeader(http.StatusInternalServerError)
4961
return
5062
}
51-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
52-
json.NewEncoder(w).Encode(&category)
5363

64+
var result Dto1
65+
args := map[string]interface{}{
66+
"collectionId": chi.URLParam(r, "collectionId"),
67+
}
68+
err = nstmt.Get(&result, args)
69+
switch err {
70+
case sql.ErrNoRows:
71+
w.WriteHeader(http.StatusNotFound)
72+
case nil:
73+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
74+
json.NewEncoder(w).Encode(&result)
75+
default:
76+
fmt.Fprintf(os.Stderr, "Get failed: %v\n", err)
77+
w.WriteHeader(http.StatusInternalServerError)
78+
}
5479
})
5580

5681
r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) {
57-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
58-
list := []Category{categories[1]}
59-
json.NewEncoder(w).Encode(&list)
60-
82+
var result []Dto3
83+
err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories")
84+
switch err {
85+
case sql.ErrNoRows:
86+
w.WriteHeader(http.StatusNotFound)
87+
case nil:
88+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
89+
json.NewEncoder(w).Encode(&result)
90+
default:
91+
fmt.Fprintf(os.Stderr, "Select failed: %v\n", err)
92+
w.WriteHeader(http.StatusInternalServerError)
93+
}
6194
})
6295

6396
r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) {
@@ -70,15 +103,28 @@ func registerRoutes(r chi.Router) {
70103
})
71104

72105
r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) {
73-
id, _ := strconv.Atoi(chi.URLParam(r, "categoryId"))
74-
category, exist := categories[id]
75-
if !exist {
76-
w.WriteHeader(http.StatusNotFound)
106+
nstmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId")
107+
if err != nil {
108+
fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err)
109+
w.WriteHeader(http.StatusInternalServerError)
77110
return
78111
}
79-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
80-
json.NewEncoder(w).Encode(&category)
81112

113+
var result Dto3
114+
args := map[string]interface{}{
115+
"categoryId": chi.URLParam(r, "categoryId"),
116+
}
117+
err = nstmt.Get(&result, args)
118+
switch err {
119+
case sql.ErrNoRows:
120+
w.WriteHeader(http.StatusNotFound)
121+
case nil:
122+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
123+
json.NewEncoder(w).Encode(&result)
124+
default:
125+
fmt.Fprintf(os.Stderr, "Get failed: %v\n", err)
126+
w.WriteHeader(http.StatusInternalServerError)
127+
}
82128
})
83129

84130
r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) {

src/cli.js

+10
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ const createEndpoints = async (destDir, lang, config) => {
127127
: params;
128128
},
129129

130+
// [ "p.page", "b.num" ] => 'chi.URLParam(r, "page"), chi.URLParam(r, "num")'
131+
// (used only with Golang's go-chi)
132+
// TODO: do we need to de-deduplicate (new Set(params))?
133+
// TODO: handle b.params
134+
"formatParamsAsGolangVararg": (params) => {
135+
return params.length > 0
136+
? Array.from(params, p => `chi.URLParam(r, "${p.substring(2)}")`).join(', ')
137+
: params;
138+
},
139+
130140
// "SELECT *\n FROM foo" => "SELECT * FROM foo"
131141
"formatQuery": (query) => {
132142
return removePlaceholders(flattenQuery(query));

src/templates/app.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package main
22

3-
import "database/sql"
43
import "fmt"
54
import "net/http"
65
import "os"
76
import "github.com/go-chi/chi"
7+
import "github.com/jmoiron/sqlx"
88

99
import _ "github.com/go-sql-driver/mysql"
1010

@@ -26,9 +26,9 @@ func main() {
2626
}
2727

2828
dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper)
29-
db, err := sql.Open("mysql", dsn)
29+
db, err := sqlx.Open("mysql", dsn)
3030
if err != nil {
31-
fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err)
31+
fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err)
3232
os.Exit(1)
3333
}
3434
defer db.Close()
@@ -39,7 +39,7 @@ func main() {
3939
}
4040

4141
r := chi.NewRouter()
42-
registerRoutes(r)
42+
registerRoutes(r, db)
4343

4444
port := os.Getenv("PORT")
4545
if port == "" {

src/templates/go.mod.ejs

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ go 1.14
55
require (
66
github.com/go-chi/chi v4.1.2+incompatible
77
github.com/go-sql-driver/mysql v1.5.0
8+
github.com/jmoiron/sqlx v1.2.0
89
)

src/templates/routes.go.ejs

+49-30
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package main
22

3+
import "database/sql"
34
import "encoding/json"
45
import "fmt"
56
import "net/http"
7+
import "os"
68
import "strconv"
79
import "github.com/go-chi/chi"
10+
import "github.com/jmoiron/sqlx"
811

912
type Category struct {
10-
Id int `json:"id"`
11-
Name *string `json:"name"`
12-
NameRu *string `json:"name_ru"`
13-
Slug *string `json:"slug"`
13+
Id int `json:"id" db:"id"`
14+
Name string `json:"name" db:"name"`
15+
NameRu *string `json:"name_ru" db:"name_ru"`
16+
Slug string `json:"slug" db:"slug"`
1417
}
1518

1619
<%
@@ -79,15 +82,15 @@ function addTypes(props) {
7982
return {
8083
"name": prop,
8184
// TODO: resolve/autoguess types
82-
"type": "string"
85+
"type": "*string"
8386
}
8487
});
8588
}
8689
8790
function query2dto(parser, query) {
8891
query = removePlaceholders(query);
8992
const queryAst = parser.astify(query);
90-
const props = extractProperties(queryAst).map(snake2camelCase);
93+
const props = extractProperties(queryAst);
9194
if (props.length === 0) {
9295
console.warn('Could not create DTO for query:', formatQuery(query));
9396
console.debug('Query AST:');
@@ -118,9 +121,11 @@ function snake2camelCase(str) {
118121
return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase());
119122
}
120123
121-
// ["a", "bb", "ccc"] => 3
124+
// ["a", "b__b", "ccc"] => 3
125+
// Note that it doesn't count underscores.
122126
function lengthOfLongestString(arr) {
123127
return arr
128+
.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))
124129
.map(el => el.length)
125130
.reduce(
126131
(acc, val) => val > acc ? val : acc,
@@ -132,7 +137,7 @@ function dto2struct(dto) {
132137
let result = `type ${dto.name} struct {\n`;
133138
dto.props.forEach(prop => {
134139
const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength);
135-
result += `\t${fieldName} ${prop.type} \`json:"${prop.name}"\`\n`
140+
result += `\t${fieldName} ${prop.type} \`json:"${prop.name},omitempty" db:"${prop.name}"\`\n`
136141
});
137142
result += '}\n';
138143
@@ -166,7 +171,7 @@ endpoints.forEach(function(endpoint) {
166171
});
167172
});
168173
-%>
169-
func registerRoutes(r chi.Router) {
174+
func registerRoutes(r chi.Router, db *sqlx.DB) {
170175
categories := make(map[int]Category)
171176
cnt := 0
172177
<%
@@ -177,33 +182,47 @@ endpoints.forEach(function(endpoint) {
177182
const hasGetOne = method.name === 'get';
178183
const hasGetMany = method.name === 'get_list';
179184
if (hasGetOne || hasGetMany) {
185+
const dto = query2dto(sqlParser, method.query);
186+
// TODO: do we really need signature and cache?
187+
const cacheKey = dto ? dto.signature : null;
188+
const dataType = hasGetMany ? '[]' + dtoCache[cacheKey] : dtoCache[cacheKey];
189+
190+
const params = extractParams(method.query);
191+
const formattedParams = formatParamsAsGolangVararg(params);
192+
const queryFunction = hasGetOne ? 'Get' : 'Select';
193+
// TODO: handle only particular method (get/post/put)
194+
// TODO: include method/path into an error message
180195
%>
181196
r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) {
182197
<%
183-
if (path === '/v1/categories/count') {
184-
-%>
185-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
186-
fmt.Fprintf(w, `{"counter": %d}`, len(categories))
187-
<%
188-
} else if (hasGetMany) {
198+
if (params.length > 0) {
189199
-%>
190-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
191-
list := []Category{categories[1]}
192-
json.NewEncoder(w).Encode(&list)
193-
<%
194-
} else {
195-
-%>
196-
id, _ := strconv.Atoi(chi.URLParam(r, "categoryId"))
197-
category, exist := categories[id]
198-
if !exist {
199-
w.WriteHeader(http.StatusNotFound)
200+
nstmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>")
201+
if err != nil {
202+
fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err)
203+
w.WriteHeader(http.StatusInternalServerError)
200204
return
201205
}
202-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
203-
json.NewEncoder(w).Encode(&category)
204-
<%
205-
}
206-
%>
206+
207+
var result <%- dataType %>
208+
args := map[string]interface{}{
209+
<%- params.map(p => `"${p.substring(2)}": chi.URLParam(r, "${p.substring(2)}"),`).join('\n\t\t\t') %>
210+
}
211+
err = nstmt.Get(&result, args)
212+
<% } else { -%>
213+
var result <%- dataType %>
214+
err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>")
215+
<% } -%>
216+
switch err {
217+
case sql.ErrNoRows:
218+
w.WriteHeader(http.StatusNotFound)
219+
case nil:
220+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
221+
json.NewEncoder(w).Encode(&result)
222+
default:
223+
fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err)
224+
w.WriteHeader(http.StatusInternalServerError)
225+
}
207226
})
208227
<%
209228
}

0 commit comments

Comments
 (0)