Skip to content

Commit 9dfae6b

Browse files
committed
chore(golang): generate DTOs based on queries
Part of #9
1 parent ebac727 commit 9dfae6b

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

examples/go/routes.go

+18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ type Category struct {
1313
Slug *string `json:"slug"`
1414
}
1515

16+
type Dto1 struct {
17+
Counter string `json:"counter"`
18+
}
19+
20+
type Dto3 struct {
21+
Id string `json:"id"`
22+
Name string `json:"name"`
23+
NameRu string `json:"nameRu"`
24+
Slug string `json:"slug"`
25+
}
26+
27+
type Dto4 struct {
28+
Name string `json:"name"`
29+
NameRu string `json:"nameRu"`
30+
Slug string `json:"slug"`
31+
UserId string `json:"userId"`
32+
}
33+
1634
func registerRoutes(r chi.Router) {
1735
categories := make(map[int]Category)
1836
cnt := 0

package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"ejs": "~3.1.3",
3434
"js-yaml": "~3.14.0",
35-
"minimist": "~1.2.5"
35+
"minimist": "~1.2.5",
36+
"node-sql-parser": "~3.0.4"
3637
}
3738
}

src/cli.js

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const path = require('path');
77

88
const parseArgs = require('minimist');
99

10+
const { Parser } = require('node-sql-parser');
11+
1012
const endpointsFile = 'endpoints.yaml';
1113

1214
const parseCommandLineArgs = (args) => {
@@ -83,6 +85,8 @@ const createEndpoints = async (destDir, lang, config) => {
8385
'b': 'req.body'
8486
}
8587

88+
const parser = new Parser();
89+
8690
const resultedCode = await ejs.renderFile(
8791
`${__dirname}/templates/routes.${lang}.ejs`,
8892
{
@@ -105,6 +109,12 @@ const createEndpoints = async (destDir, lang, config) => {
105109

106110
// (used only with Golang's go-chi)
107111
"convertPathPlaceholders": convertPathPlaceholders,
112+
113+
// (used only with Golang)
114+
"sqlParser": parser,
115+
116+
// (used only with Golang)
117+
"removePlaceholders": removePlaceholders,
108118
}
109119
);
110120

src/templates/routes.go.ejs

+151
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,157 @@ type Category struct {
1313
Slug *string `json:"slug"`
1414
}
1515

16+
<%
17+
// {'columns':
18+
// [
19+
// {
20+
// expr: { type: 'column_ref', table: null, column: 'name_ru' },
21+
// as: 'nameRu'
22+
// }
23+
// ]
24+
// } => [ 'nameRu' ]
25+
function extractSelectParameters(queryAst) {
26+
return queryAst.columns
27+
.map(column => column.as !== null ? column.as : column.expr.column);
28+
}
29+
30+
// {'values':
31+
// [
32+
// {
33+
// type: 'expr_list',
34+
// value: [ { type: 'param', value: 'user_id' } ]
35+
// }
36+
// ]
37+
// } => [ 'user_id' ]
38+
function extractInsertValues(queryAst) {
39+
const values = queryAst.values.flatMap(elem => elem.value)
40+
.map(elem => elem.type === 'param' ? elem.value : null)
41+
.filter(elem => elem); // filter out nulls
42+
return Array.from(new Set(values));
43+
}
44+
45+
// {'set':
46+
// [
47+
// {
48+
// column: 'updated_by',
49+
// value: { type: 'param', value: 'user_id' },
50+
// table: null
51+
// }
52+
// ]
53+
// } => [ 'user_id' ]
54+
function extractUpdateValues(queryAst) {
55+
// TODO: distinguish between b.param and q.param and extract only first
56+
return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null)
57+
.filter(value => value) // filter out nulls
58+
}
59+
60+
function extractProperties(queryAst) {
61+
if (queryAst.type === 'select') {
62+
return extractSelectParameters(queryAst);
63+
}
64+
65+
if (queryAst.type === 'insert') {
66+
return extractInsertValues(queryAst);
67+
}
68+
69+
if (queryAst.type === 'update') {
70+
return extractUpdateValues(queryAst);
71+
}
72+
73+
return [];
74+
}
75+
76+
function addTypes(props) {
77+
return props.map(prop => {
78+
return {
79+
"name": prop,
80+
// TODO: resolve/autoguess types
81+
"type": "string"
82+
}
83+
});
84+
}
85+
86+
function query2dto(parser, query) {
87+
const queryAst = parser.astify(query);
88+
const props = extractProperties(queryAst).map(snake2camelCase);
89+
if (props.length === 0) {
90+
console.warn('Could not create DTO for query:', formatQuery(query));
91+
console.debug('Query AST:');
92+
console.debug(queryAst);
93+
return null;
94+
}
95+
const propsWithTypes = addTypes(props);
96+
return {
97+
// TODO: assign DTO name dynamically
98+
"name": "Dto" + ++globalDtoCounter,
99+
"props": propsWithTypes,
100+
// max length is needed for proper formatting
101+
"maxFieldNameLength": lengthOfLongestString(props),
102+
// required for de-duplication
103+
// [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string"
104+
// TODO: sort before join
105+
"signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ')
106+
};
107+
}
108+
109+
// "nameRu" => "NameRu"
110+
function capitalize(str) {
111+
return str[0].toUpperCase() + str.slice(1);
112+
}
113+
114+
// "name_ru" => "nameRu"
115+
function snake2camelCase(str) {
116+
return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase());
117+
}
118+
119+
// ["a", "bb", "ccc"] => 3
120+
function lengthOfLongestString(arr) {
121+
return arr
122+
.map(el => el.length)
123+
.reduce(
124+
(acc, val) => val > acc ? val : acc,
125+
0 /* initial value */
126+
);
127+
}
128+
129+
function dto2struct(dto) {
130+
let result = `type ${dto.name} struct {\n`;
131+
dto.props.forEach(prop => {
132+
const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength);
133+
result += `\t${fieldName} ${prop.type} \`json:"${prop.name}"\`\n`
134+
});
135+
result += '}\n';
136+
137+
return result;
138+
}
139+
140+
let globalDtoCounter = 0;
141+
142+
const dtoCache = {};
143+
function cacheDto(dto) {
144+
dtoCache[dto.signature] = dto.name;
145+
return dto;
146+
}
147+
function dtoInCache(dto) {
148+
return dtoCache.hasOwnProperty(dto.signature);
149+
}
150+
151+
const verbs_with_dto = [ 'get', 'get_list', 'post', 'put' ]
152+
endpoints.forEach(function(endpoint) {
153+
const dtos = Object.keys(endpoint)
154+
.filter(propName => verbs_with_dto.includes(propName))
155+
.map(propName => endpoint[propName])
156+
.map(query => query2dto(sqlParser, removePlaceholders(query)))
157+
.filter(elem => elem) // filter out nulls
158+
.filter(dto => !dtoInCache(dto))
159+
.map(dto => dto2struct(cacheDto(dto)))
160+
.forEach(struct => {
161+
-%>
162+
<%- struct %>
163+
<%
164+
});
165+
});
166+
-%>
16167
func registerRoutes(r chi.Router) {
17168
categories := make(map[int]Category)
18169
cnt := 0

0 commit comments

Comments
 (0)