forked from php-coder/query2app
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcli.js
executable file
·315 lines (268 loc) · 9.61 KB
/
cli.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#!/usr/bin/env node
const yaml = require('js-yaml');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const parseArgs = require('minimist');
const { Parser } = require('node-sql-parser');
const endpointsFile = 'endpoints.yaml';
const parseCommandLineArgs = (args) => {
const opts = {
'string': [ 'lang' ],
'default': {
'lang': 'js'
}
};
const argv = parseArgs(args, opts);
//console.debug('argv:', argv);
return argv;
}
// Restructure YAML configuration to simplify downstream code.
//
// Converts
// {
// get_list: { query: <sql> },
// put: { query: <sql> }
// }
// into
// {
// methods: [
// { name: get_list, verb: get, query: <sql> },
// { name: put, verb: put, query: <sql> }
// ]
// }
const restructureConfiguration = (config) => {
for (const endpoint of config) {
endpoint.methods = [];
[ 'get', 'get_list', 'post', 'put', 'delete' ].forEach(method => {
if (!endpoint.hasOwnProperty(method)) {
return;
}
endpoint.methods.push({
'name': method,
'verb': method !== 'get_list' ? method : 'get',
...endpoint[method],
});
delete endpoint[method];
});
}
};
const loadConfig = (endpointsFile) => {
console.log('Read', endpointsFile);
try {
const content = fs.readFileSync(endpointsFile, 'utf8');
const config = yaml.safeLoad(content);
restructureConfiguration(config);
//console.debug(config);
return config;
} catch (ex) {
console.error(`Failed to parse ${endpointsFile}: ${ex.message}`);
throw ex;
}
};
const lang2extension = (lang) => {
switch (lang) {
case 'js':
case 'go':
return lang
case 'python':
return 'py'
default:
throw new Error(`Unsupported language: ${lang}`)
}
}
const createApp = async (destDir, lang) => {
const ext = lang2extension(lang)
const fileName = `app.${ext}`
console.log('Generate', fileName);
const resultFile = path.join(destDir, fileName);
fs.copyFileSync(`${__dirname}/templates/app.${ext}`, resultFile)
};
const removeComments = (query) => query.replace(/--.*\n/g, '');
// "SELECT *\n FROM foo" => "SELECT * FROM foo"
const flattenQuery = (query) => query.replace(/\n[ ]*/g, ' ');
// "WHERE id = :p.categoryId OR id = :b.id" => "WHERE id = :categoryId OR id = :id"
const removePlaceholders = (query) => query.replace(/(?<=:)[pb]\./g, '');
// "/categories/:id" => "/categories/{id}"
// (used only with Golang's go-chi)
const convertPathPlaceholders = (path) => path.replace(/:([^\/]+)/g, '{$1}');
// "name_ru" => "nameRu"
// (used only with Golang's go-chi)
const snake2camelCase = (str) => str.replace(/_([a-z])/g, (match, group) => group.toUpperCase());
// "categoryId" => "category_id"
// (used only with Python's FastAPI)
const camel2snakeCase = (str) => str.replace(/([A-Z])/g, (match, group) => '_' + group.toLowerCase());
// "nameRu" => "NameRu"
// (used only with Golang's go-chi)
const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
// ["a", "bb", "ccc"] => 3
// (used only with Golang's go-chi)
const lengthOfLongestString = (arr) => arr
.map(el => el.length)
.reduce(
(acc, val) => val > acc ? val : acc,
0 /* initial value */
);
const createEndpoints = async (destDir, lang, config) => {
const ext = lang2extension(lang)
const fileName = `routes.${ext}`
console.log('Generate', fileName);
const resultFile = path.join(destDir, fileName);
for (let endpoint of config) {
let path = endpoint.path;
if (lang === 'go') {
path = convertPathPlaceholders(path)
}
endpoint.methods.forEach(method => {
const verb = method.verb.toUpperCase();
console.log(`${verb} ${path}`);
let queries = []
if (method.query) {
queries.push(method.query)
} else if (method.aggregated_queries) {
queries = Object.values(method.aggregated_queries)
}
queries.forEach(query => {
const sql = removePlaceholders(flattenQuery(removeComments(query)));
console.log(`\t${sql}`);
})
});
}
const placeholdersMap = {
'js': {
'p': 'req.params',
'b': 'req.body',
},
'go': {
'p': function(param) {
return `chi.URLParam(r, "${param}")`
},
'b': function(param) {
return 'dto.' + capitalize(snake2camelCase(param));
},
}
}
const parser = new Parser();
const resultedCode = await ejs.renderFile(
`${__dirname}/templates/routes.${ext}.ejs`,
{
"endpoints": config,
// "... WHERE id = :p.id" => [ "p.id" ]
"extractParamsFromQuery": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [],
// "/categories/:categoryId" => [ "categoryId" ]
// (used only with FastAPI)
"extractParamsFromPath": (query) => query.match(/(?<=:)\w+/g) || [],
// [ "p.page", "b.num" ] => '"page": req.params.page, "num": req.body.num'
// (used only with Express)
"formatParamsAsJavaScriptObject": (params) => {
if (params.length === 0) {
return params;
}
return Array.from(
new Set(params),
p => {
const bindTarget = p.substring(0, 1);
const paramName = p.substring(2);
const prefix = placeholdersMap['js'][bindTarget];
return `"${paramName}": ${prefix}.${paramName}`
}
).join(', ');
},
// "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id"
"formatQuery": (query) => {
return removePlaceholders(flattenQuery(removeComments(query)));
},
// (used only with Golang)
"convertPathPlaceholders": convertPathPlaceholders,
"sqlParser": parser,
"removePlaceholders": removePlaceholders,
"snake2camelCase": snake2camelCase,
"capitalize": capitalize,
"lengthOfLongestString": lengthOfLongestString,
// used only with Pyth
"camel2snakeCase": camel2snakeCase,
// [ "p.page", "b.num" ] => '"page": chi.URLParam(r, "page"),\n\t\t\t"num": dto.Num),'
// (used only with Golang's go-chi)
"formatParamsAsGolangMap": (params) => {
if (params.length === 0) {
return params;
}
const maxParamNameLength = lengthOfLongestString(params);
return Array.from(
new Set(params),
p => {
const bindTarget = p.substring(0, 1);
const paramName = p.substring(2);
const formatFunc = placeholdersMap['go'][bindTarget];
const quotedParam = '"' + paramName + '":';
// We don't count quotes and colon because they are compensated by "p." prefix.
// We do +1 because the longest parameter will also have an extra space as a delimiter.
return `${quotedParam.padEnd(maxParamNameLength+1)} ${formatFunc(paramName)},`
}
).join('\n\t\t\t');
},
}
);
fs.writeFileSync(resultFile, resultedCode);
};
const createDependenciesDescriptor = async (destDir, lang) => {
let fileName;
if (lang === 'js') {
fileName = 'package.json'
} else if (lang === 'go') {
fileName = 'go.mod'
} else if (lang === 'python') {
fileName = 'requirements.txt'
} else {
return;
}
console.log('Generate', fileName);
const resultFile = path.join(destDir, fileName);
const projectName = path.basename(destDir);
console.log('Project name:', projectName);
const minimalPackageJson = await ejs.renderFile(
`${__dirname}/templates/${fileName}.ejs`,
{
// project name is being used only for package.json
projectName
}
);
fs.writeFileSync(resultFile, minimalPackageJson);
};
const showInstructions = (lang) => {
console.info('The application has been generated!')
if (lang === 'js') {
console.info(`Use
npm install
to install its dependencies and
export DB_NAME=db DB_USER=user DB_PASSWORD=secret
npm start
afteward to run`);
} else if (lang === 'go') {
console.info(`Use
go run *.go
or
go build -o app
./app
to build and run it`)
} else if (lang === 'python') {
console.info(`Use
pip install -r requirements.txt
to install its dependencies and
uvicorn app:app
afteward to run`)
}
};
const argv = parseCommandLineArgs(process.argv.slice(2));
const config = loadConfig(endpointsFile);
let destDir = argv._.length > 0 ? argv._[0] : '.';
destDir = path.resolve(process.cwd(), destDir);
console.log('Destination directory:', destDir)
if (!fs.existsSync(destDir)) {
console.log('Create', destDir)
fs.mkdirSync(destDir, {recursive: true});
}
createApp(destDir, argv.lang, config);
createEndpoints(destDir, argv.lang, config);
createDependenciesDescriptor(destDir, argv.lang);
showInstructions(argv.lang);