From b2cdc3731e490e95321c04eb99861aa96451ff30 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 26 Jul 2020 19:51:19 +0100 Subject: [PATCH 001/346] chore: include only code into npm package This excludes directories docker, examples and tests from a package that is published with npm publish --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index dd5779a..f267e25 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, + "files": [ + "src/**" + ], "dependencies": { "ejs": "~3.1.3", "js-yaml": "~3.14.0" From b2a7ac330652ba347f19e2f362b1e9ffde8ad803 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 26 Jul 2020 19:59:17 +0100 Subject: [PATCH 002/346] style: sort package.json Done by npx sort-package-json command. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f267e25..1e8fe9d 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "bin": { "query2app": "./src/cli.js" }, + "files": [ + "src/**" + ], "scripts": { "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, - "files": [ - "src/**" - ], "dependencies": { "ejs": "~3.1.3", "js-yaml": "~3.14.0" From a1a6abe51c1f6411ac4bba627ec66a079cde4016 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 28 Jul 2020 22:22:13 +0100 Subject: [PATCH 003/346] chore: add --lang option At this moment it defaults to "js" and everything works as before. But if you pass --lang go it won't generate JS-related files. Part of #9 --- package-lock.json | 5 +++++ package.json | 3 ++- src/cli.js | 32 ++++++++++++++++++++++++++------ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index aba5a2f..b4215f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,11 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 1e8fe9d..1e0262f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "ejs": "~3.1.3", - "js-yaml": "~3.14.0" + "js-yaml": "~3.14.0", + "minimist": "~1.2.5" } } diff --git a/src/cli.js b/src/cli.js index 499715e..9773a14 100755 --- a/src/cli.js +++ b/src/cli.js @@ -5,10 +5,24 @@ const ejs = require('ejs'); const fs = require('fs'); const path = require('path'); +const parseArgs = require('minimist'); + const endpointsFile = 'endpoints.yaml'; const appFile = 'app.js'; const routesFile = 'routes.js'; +const parseCommandLineArgs = (args) => { + const opts = { + 'string': [ 'lang' ], + 'default': { + 'lang': 'js' + } + }; + const argv = parseArgs(args, opts); + //console.debug('argv:', argv); + return argv; +} + const loadConfig = (endpointsFile) => { console.log('Read', endpointsFile); try { @@ -108,9 +122,11 @@ const createPackageJson = async (destDir, fileName) => { fs.writeFileSync(resultFile, minimalPackageJson); }; +const argv = parseCommandLineArgs(process.argv.slice(2)); + const config = loadConfig(endpointsFile); -let [,, destDir = '.'] = process.argv; +let destDir = argv._.length > 0 ? argv._[0] : '.'; destDir = path.resolve(process.cwd(), destDir); console.log('Destination directory:', destDir) @@ -119,14 +135,18 @@ if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, {recursive: true}); } -createApp(destDir, appFile, config); -createEndpoints(destDir, routesFile, config); -createPackageJson(destDir, 'package.json'); +if (argv.lang === 'js') { + createApp(destDir, appFile, config); + createEndpoints(destDir, routesFile, config); + createPackageJson(destDir, 'package.json'); +} -console.info(`The application has been generated! -Use +console.info('The application has been generated!') +if (argv.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 it`); +} From 933e7af6dd3a53ef606a235348fc9907ee6547e6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 29 Jul 2020 22:00:17 +0100 Subject: [PATCH 004/346] refactor: move logic for choosing a filename into createApp() --- src/cli.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.js b/src/cli.js index 9773a14..4090685 100755 --- a/src/cli.js +++ b/src/cli.js @@ -8,7 +8,6 @@ const path = require('path'); const parseArgs = require('minimist'); const endpointsFile = 'endpoints.yaml'; -const appFile = 'app.js'; const routesFile = 'routes.js'; const parseCommandLineArgs = (args) => { @@ -36,7 +35,8 @@ const loadConfig = (endpointsFile) => { } }; -const createApp = async (destDir, fileName) => { +const createApp = async (destDir, lang) => { + const fileName = `app.${lang}` console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -136,7 +136,7 @@ if (!fs.existsSync(destDir)) { } if (argv.lang === 'js') { - createApp(destDir, appFile, config); + createApp(destDir, argv.lang, config); createEndpoints(destDir, routesFile, config); createPackageJson(destDir, 'package.json'); } From ffacf9c91d56e86da14ae24a8a18e3ac6df6ec9b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 29 Jul 2020 22:02:51 +0100 Subject: [PATCH 005/346] refactor: use language in a template path for app file --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 4090685..1324ce0 100755 --- a/src/cli.js +++ b/src/cli.js @@ -40,7 +40,7 @@ const createApp = async (destDir, lang) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); - fs.copyFileSync(__dirname + '/templates/app.js', resultFile) + fs.copyFileSync(`${__dirname}/templates/app.${lang}`, resultFile) }; // "SELECT *\n FROM foo" => "SELECT * FROM foo" From f9857f29d70d25eb6968c13b2292d8aa13bab8da Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 29 Jul 2020 22:21:12 +0100 Subject: [PATCH 006/346] chore: allow to generate "hello world" app on Golang Part of #9 --- examples/go/app.go | 7 +++++++ examples/go/endpoints.yaml | 1 + src/cli.js | 11 +++++++++-- src/templates/app.go | 7 +++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 examples/go/app.go create mode 120000 examples/go/endpoints.yaml create mode 100644 src/templates/app.go diff --git a/examples/go/app.go b/examples/go/app.go new file mode 100644 index 0000000..31e5646 --- /dev/null +++ b/examples/go/app.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Listen on 3000") +} diff --git a/examples/go/endpoints.yaml b/examples/go/endpoints.yaml new file mode 120000 index 0000000..ff2e3db --- /dev/null +++ b/examples/go/endpoints.yaml @@ -0,0 +1 @@ +../js/endpoints.yaml \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index 1324ce0..b6a918c 100755 --- a/src/cli.js +++ b/src/cli.js @@ -135,8 +135,8 @@ if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, {recursive: true}); } +createApp(destDir, argv.lang, config); if (argv.lang === 'js') { - createApp(destDir, argv.lang, config); createEndpoints(destDir, routesFile, config); createPackageJson(destDir, 'package.json'); } @@ -148,5 +148,12 @@ if (argv.lang === 'js') { to install its dependencies and export DB_NAME=db DB_USER=user DB_PASSWORD=secret npm start -afteward to run it`); +afteward to run`); +} else if (argv.lang === 'go') { + console.info(`Use + go run app.go +or + go build -o app + ./app +to build and run it`) } diff --git a/src/templates/app.go b/src/templates/app.go new file mode 100644 index 0000000..31e5646 --- /dev/null +++ b/src/templates/app.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Listen on 3000") +} From 8a8e82212bca221b5f61fad454e6e0922f943808 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 29 Jul 2020 22:28:43 +0100 Subject: [PATCH 007/346] refactor: extract showInstructions() function --- src/cli.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/cli.js b/src/cli.js index b6a918c..d6c237e 100755 --- a/src/cli.js +++ b/src/cli.js @@ -122,6 +122,26 @@ const createPackageJson = async (destDir, fileName) => { fs.writeFileSync(resultFile, minimalPackageJson); }; +const showInstructions = (lang) => { + console.info('The application has been generated!') + if (argv.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 (argv.lang === 'go') { + console.info(`Use + go run app.go +or + go build -o app + ./app +to build and run it`) + } +}; + + const argv = parseCommandLineArgs(process.argv.slice(2)); const config = loadConfig(endpointsFile); @@ -141,19 +161,4 @@ if (argv.lang === 'js') { createPackageJson(destDir, 'package.json'); } -console.info('The application has been generated!') -if (argv.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 (argv.lang === 'go') { - console.info(`Use - go run app.go -or - go build -o app - ./app -to build and run it`) -} +showInstructions(argv.lang); From e4094cb6121aee6c2abe2e42c76765cb0ede2ae8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 4 Aug 2020 22:27:38 +0100 Subject: [PATCH 008/346] refactor: move logic for choosing a filename into createEndpoints() --- src/cli.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli.js b/src/cli.js index d6c237e..e3dc5d2 100755 --- a/src/cli.js +++ b/src/cli.js @@ -8,7 +8,6 @@ const path = require('path'); const parseArgs = require('minimist'); const endpointsFile = 'endpoints.yaml'; -const routesFile = 'routes.js'; const parseCommandLineArgs = (args) => { const opts = { @@ -49,7 +48,8 @@ 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, ':'); -const createEndpoints = async (destDir, fileName, config) => { +const createEndpoints = async (destDir, lang, config) => { + const fileName = `routes.${lang}` console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -76,7 +76,7 @@ const createEndpoints = async (destDir, fileName, config) => { } const resultedCode = await ejs.renderFile( - __dirname + '/templates/routes.js.ejs', + `${__dirname}/templates/routes.${lang}.ejs`, { "endpoints": config, @@ -157,7 +157,7 @@ if (!fs.existsSync(destDir)) { createApp(destDir, argv.lang, config); if (argv.lang === 'js') { - createEndpoints(destDir, routesFile, config); + createEndpoints(destDir, argv.lang, config); createPackageJson(destDir, 'package.json'); } From dbd7326006fd5e70a8826218987448205b59e30a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 4 Aug 2020 22:38:46 +0100 Subject: [PATCH 009/346] chore(golang): generate stubs for routes Part of #9 --- examples/go/app.go | 8 ++++++++ examples/go/routes.go | 23 +++++++++++++++++++++++ src/cli.js | 4 ++-- src/templates/app.go | 8 ++++++++ src/templates/routes.go.ejs | 11 +++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 examples/go/routes.go create mode 100644 src/templates/routes.go.ejs diff --git a/examples/go/app.go b/examples/go/app.go index 31e5646..371907b 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -1,7 +1,15 @@ package main import "fmt" +import "net/http" +import "os" func main() { + registerRoutes() + fmt.Println("Listen on 3000") + if err := http.ListenAndServe(":3000", nil); err != nil { + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) + os.Exit(1) + } } diff --git a/examples/go/routes.go b/examples/go/routes.go new file mode 100644 index 0000000..da6c3d8 --- /dev/null +++ b/examples/go/routes.go @@ -0,0 +1,23 @@ +package main + +import "net/http" + +func registerRoutes() { + + http.HandleFunc("/v1/categories/count", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + http.HandleFunc("/v1/collections/:collectionId/categories/count", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + http.HandleFunc("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + http.HandleFunc("/v1/categories/:categoryId", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + +} diff --git a/src/cli.js b/src/cli.js index e3dc5d2..c5b4276 100755 --- a/src/cli.js +++ b/src/cli.js @@ -133,7 +133,7 @@ to install its dependencies and afteward to run`); } else if (argv.lang === 'go') { console.info(`Use - go run app.go + go run *.go or go build -o app ./app @@ -156,8 +156,8 @@ if (!fs.existsSync(destDir)) { } createApp(destDir, argv.lang, config); +createEndpoints(destDir, argv.lang, config); if (argv.lang === 'js') { - createEndpoints(destDir, argv.lang, config); createPackageJson(destDir, 'package.json'); } diff --git a/src/templates/app.go b/src/templates/app.go index 31e5646..371907b 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -1,7 +1,15 @@ package main import "fmt" +import "net/http" +import "os" func main() { + registerRoutes() + fmt.Println("Listen on 3000") + if err := http.ListenAndServe(":3000", nil); err != nil { + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) + os.Exit(1) + } } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs new file mode 100644 index 0000000..19b16e5 --- /dev/null +++ b/src/templates/routes.go.ejs @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func registerRoutes() { +<% endpoints.forEach(function(endpoint) { %> + http.HandleFunc("<%- endpoint.path %>", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) +<% }) %> +} From b327038795d893c6d9145a9a16ba25c31ec24196 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 10:08:44 +0100 Subject: [PATCH 010/346] refactor: extract a variable --- src/cli.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli.js b/src/cli.js index c5b4276..025a1d5 100755 --- a/src/cli.js +++ b/src/cli.js @@ -54,19 +54,20 @@ const createEndpoints = async (destDir, lang, config) => { const resultFile = path.join(destDir, fileName); for (let endpoint of config) { + let path = endpoint.path; if (endpoint.hasOwnProperty('get')) { - console.log('GET', endpoint.path, '=>', removePlaceholders(flattenQuery(endpoint.get))); + console.log('GET', path, '=>', removePlaceholders(flattenQuery(endpoint.get))); } else if (endpoint.hasOwnProperty('get_list')) { - console.log('GET', endpoint.path, '=>', removePlaceholders(flattenQuery(endpoint.get_list))); + console.log('GET', path, '=>', removePlaceholders(flattenQuery(endpoint.get_list))); } if (endpoint.hasOwnProperty('post')) { - console.log('POST', endpoint.path, '=>', removePlaceholders(flattenQuery(endpoint.post))); + console.log('POST', path, '=>', removePlaceholders(flattenQuery(endpoint.post))); } if (endpoint.hasOwnProperty('put')) { - console.log('PUT', endpoint.path, '=>', removePlaceholders(flattenQuery(endpoint.put))); + console.log('PUT', path, '=>', removePlaceholders(flattenQuery(endpoint.put))); } if (endpoint.hasOwnProperty('delete')) { - console.log('DELETE', endpoint.path, '=>', removePlaceholders(flattenQuery(endpoint.delete))); + console.log('DELETE', path, '=>', removePlaceholders(flattenQuery(endpoint.delete))); } } From d21a695a429b29d4727732eb63f7f74943337971 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 10:10:31 +0100 Subject: [PATCH 011/346] refactor(showInstructions): use a parameter instead of a global variable --- src/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index 025a1d5..60942c3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -125,14 +125,14 @@ const createPackageJson = async (destDir, fileName) => { const showInstructions = (lang) => { console.info('The application has been generated!') - if (argv.lang === 'js') { + 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 (argv.lang === 'go') { + } else if (lang === 'go') { console.info(`Use go run *.go or From c7c1fce1e4e1380e88c18c015ed43562e4854b24 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 10:16:13 +0100 Subject: [PATCH 012/346] chore(golang): switch to go-chi router Closes #12 Part of #9 --- examples/go/app.go | 6 ++++-- examples/go/routes.go | 23 ++++++++++++++++++----- src/cli.js | 14 +++++++++++++- src/templates/app.go | 6 ++++-- src/templates/routes.go.ejs | 37 +++++++++++++++++++++++++++++++++---- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 371907b..c0e7552 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -3,12 +3,14 @@ package main import "fmt" import "net/http" import "os" +import "github.com/go-chi/chi" func main() { - registerRoutes() + r := chi.NewRouter() + registerRoutes(r) fmt.Println("Listen on 3000") - if err := http.ListenAndServe(":3000", nil); err != nil { + if err := http.ListenAndServe(":3000", r); err != nil { fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) os.Exit(1) } diff --git a/examples/go/routes.go b/examples/go/routes.go index da6c3d8..5827231 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -1,22 +1,35 @@ package main import "net/http" +import "github.com/go-chi/chi" -func registerRoutes() { +func registerRoutes(r chi.Router) { - http.HandleFunc("/v1/categories/count", func(w http.ResponseWriter, _ *http.Request) { + r.Get("/v1/categories/count", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("TODO")) }) - http.HandleFunc("/v1/collections/:collectionId/categories/count", func(w http.ResponseWriter, _ *http.Request) { + r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("TODO")) }) - http.HandleFunc("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { + r.Get("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("TODO")) }) - http.HandleFunc("/v1/categories/:categoryId", func(w http.ResponseWriter, _ *http.Request) { + r.Post("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) + + r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("TODO")) }) diff --git a/src/cli.js b/src/cli.js index 60942c3..2a3006f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -48,6 +48,12 @@ 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) => { + return path.replace(/:[^\/]+/g, (placeholder) => '{' + placeholder.slice(1) + '}'); +}; + const createEndpoints = async (destDir, lang, config) => { const fileName = `routes.${lang}` console.log('Generate', fileName); @@ -55,6 +61,9 @@ const createEndpoints = async (destDir, lang, config) => { for (let endpoint of config) { let path = endpoint.path; + if (lang === 'go') { + path = convertPathPlaceholders(path) + } if (endpoint.hasOwnProperty('get')) { console.log('GET', path, '=>', removePlaceholders(flattenQuery(endpoint.get))); } else if (endpoint.hasOwnProperty('get_list')) { @@ -99,7 +108,10 @@ const createEndpoints = async (destDir, lang, config) => { // "SELECT *\n FROM foo" => "'SELECT * FROM foo'" "formatQuery": (query) => { return "'" + removePlaceholders(flattenQuery(query)) + "'"; - } + }, + + // (used only with Golang's go-chi) + "convertPathPlaceholders": convertPathPlaceholders, } ); diff --git a/src/templates/app.go b/src/templates/app.go index 371907b..c0e7552 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -3,12 +3,14 @@ package main import "fmt" import "net/http" import "os" +import "github.com/go-chi/chi" func main() { - registerRoutes() + r := chi.NewRouter() + registerRoutes(r) fmt.Println("Listen on 3000") - if err := http.ListenAndServe(":3000", nil); err != nil { + if err := http.ListenAndServe(":3000", r); err != nil { fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) os.Exit(1) } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 19b16e5..dd8618f 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -1,11 +1,40 @@ package main import "net/http" +import "github.com/go-chi/chi" -func registerRoutes() { -<% endpoints.forEach(function(endpoint) { %> - http.HandleFunc("<%- endpoint.path %>", func(w http.ResponseWriter, _ *http.Request) { +func registerRoutes(r chi.Router) { +<% +endpoints.forEach(function(endpoint) { + const path = convertPathPlaceholders(endpoint.path); + if (endpoint.hasOwnProperty('get') || endpoint.hasOwnProperty('get_list')) { +%> + r.Get("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("TODO")) }) -<% }) %> +<% + } + if (endpoint.hasOwnProperty('post')) { +%> + r.Post("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) +<% + } + if (endpoint.hasOwnProperty('put')) { +%> + r.Put("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) +<% + } + if (endpoint.hasOwnProperty('delete')) { +%> + r.Delete("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("TODO")) + }) +<% + } +}) +%> } From e77de6f8c24df54ea84c4ac5b3db07aa8fcfaf55 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 10:28:04 +0100 Subject: [PATCH 013/346] refactor: extract a variable --- src/templates/routes.js.ejs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 267f7dc..4dedbd0 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -2,13 +2,14 @@ const register = (app, pool) => { <% endpoints.forEach(function(endpoint) { + const path = endpoint.path; const hasGetOne = endpoint.hasOwnProperty('get'); const hasGetMany = endpoint.hasOwnProperty('get_list'); if (hasGetOne || hasGetMany) { const sql = hasGetOne ? endpoint.get : endpoint.get_list; const params = extractParams(sql); %> -app.get('<%- endpoint.path %>', (req, res) => { +app.get('<%- path %>', (req, res) => { pool.query( <%- formatQuery(sql) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { @@ -32,7 +33,7 @@ app.get('<%- endpoint.path %>', (req, res) => { if (endpoint.hasOwnProperty('post')) { const params = extractParams(endpoint.post); %> -app.post('<%- endpoint.path %>', (req, res) => { +app.post('<%- path %>', (req, res) => { pool.query( <%- formatQuery(endpoint.post) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { @@ -48,7 +49,7 @@ app.post('<%- endpoint.path %>', (req, res) => { if (endpoint.hasOwnProperty('put')) { const params = extractParams(endpoint.put); %> -app.put('<%- endpoint.path %>', (req, res) => { +app.put('<%- path %>', (req, res) => { pool.query( <%- formatQuery(endpoint.put) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { @@ -64,7 +65,7 @@ app.put('<%- endpoint.path %>', (req, res) => { if (endpoint.hasOwnProperty('delete')) { const params = extractParams(endpoint.delete); %> -app.delete('<%- endpoint.path %>', (req, res) => { +app.delete('<%- path %>', (req, res) => { pool.query( <%- formatQuery(endpoint.delete) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { From 8642aa9792a0b50a370296ea533e19758f8163fc Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 14:59:45 +0100 Subject: [PATCH 014/346] refactor: rename a method --- src/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index 2a3006f..78dae72 100755 --- a/src/cli.js +++ b/src/cli.js @@ -118,7 +118,7 @@ const createEndpoints = async (destDir, lang, config) => { fs.writeFileSync(resultFile, resultedCode); }; -const createPackageJson = async (destDir, fileName) => { +const createDependenciesDescriptor = async (destDir, fileName) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -171,7 +171,7 @@ if (!fs.existsSync(destDir)) { createApp(destDir, argv.lang, config); createEndpoints(destDir, argv.lang, config); if (argv.lang === 'js') { - createPackageJson(destDir, 'package.json'); + createDependenciesDescriptor(destDir, 'package.json'); } showInstructions(argv.lang); From e5d30e25590ef142f1e0550a09686c30519d884b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 15:06:25 +0100 Subject: [PATCH 015/346] refactor: move some logic into createDependenciesDescriptor() --- src/cli.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli.js b/src/cli.js index 78dae72..d310ec3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -118,7 +118,11 @@ const createEndpoints = async (destDir, lang, config) => { fs.writeFileSync(resultFile, resultedCode); }; -const createDependenciesDescriptor = async (destDir, fileName) => { +const createDependenciesDescriptor = async (destDir, lang) => { + if (argv.lang !== 'js') { + return; + } + const fileName = 'package.json'; console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -170,8 +174,5 @@ if (!fs.existsSync(destDir)) { createApp(destDir, argv.lang, config); createEndpoints(destDir, argv.lang, config); -if (argv.lang === 'js') { - createDependenciesDescriptor(destDir, 'package.json'); -} - +createDependenciesDescriptor(destDir, argv.lang); showInstructions(argv.lang); From 8b459367e05f4125ca56eb406db6aa5baaf52bc9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 15:09:42 +0100 Subject: [PATCH 016/346] refactor: make a template name based on a filename --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index d310ec3..7122ce3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -130,7 +130,7 @@ const createDependenciesDescriptor = async (destDir, lang) => { console.log('Project name:', projectName); const minimalPackageJson = await ejs.renderFile( - __dirname + '/templates/package.json.ejs', + `${__dirname}/templates/${fileName}.ejs`, { projectName } From ab8850cca3fc07ca2028c5370a1f7ce89882986f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 15:19:18 +0100 Subject: [PATCH 017/346] chore(golang): generate go.mod file Part of #9 --- examples/go/go.mod | 5 +++++ src/cli.js | 12 ++++++++++-- src/templates/go.mod.ejs | 5 +++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 examples/go/go.mod create mode 100644 src/templates/go.mod.ejs diff --git a/examples/go/go.mod b/examples/go/go.mod new file mode 100644 index 0000000..f8e94a3 --- /dev/null +++ b/examples/go/go.mod @@ -0,0 +1,5 @@ +module main + +go 1.14 + +require github.com/go-chi/chi v4.1.2+incompatible diff --git a/src/cli.js b/src/cli.js index 7122ce3..77a16fd 100755 --- a/src/cli.js +++ b/src/cli.js @@ -119,10 +119,17 @@ const createEndpoints = async (destDir, lang, config) => { }; const createDependenciesDescriptor = async (destDir, lang) => { - if (argv.lang !== 'js') { + let fileName; + if (argv.lang === 'js') { + fileName = 'package.json' + + } else if (argv.lang === 'go') { + fileName = 'go.mod' + + } else { return; } - const fileName = 'package.json'; + console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -132,6 +139,7 @@ const createDependenciesDescriptor = async (destDir, lang) => { const minimalPackageJson = await ejs.renderFile( `${__dirname}/templates/${fileName}.ejs`, { + // project name is being used only for package.json projectName } ); diff --git a/src/templates/go.mod.ejs b/src/templates/go.mod.ejs new file mode 100644 index 0000000..f8e94a3 --- /dev/null +++ b/src/templates/go.mod.ejs @@ -0,0 +1,5 @@ +module main + +go 1.14 + +require github.com/go-chi/chi v4.1.2+incompatible From 2c90e213d6d51888eb88341c0873ed1ab9049329 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 15:21:35 +0100 Subject: [PATCH 018/346] refactor: use an argument instead of a global variable --- src/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index 77a16fd..cf23660 100755 --- a/src/cli.js +++ b/src/cli.js @@ -120,10 +120,10 @@ const createEndpoints = async (destDir, lang, config) => { const createDependenciesDescriptor = async (destDir, lang) => { let fileName; - if (argv.lang === 'js') { + if (lang === 'js') { fileName = 'package.json' - } else if (argv.lang === 'go') { + } else if (lang === 'go') { fileName = 'go.mod' } else { From 53b93683507084a007db7c2410b41abdf802145b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Aug 2020 18:16:57 +0100 Subject: [PATCH 019/346] chore(golang): post/put/delete return 204 status now Part of #9 --- examples/go/routes.go | 6 +++--- src/templates/routes.go.ejs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 5827231..2e78305 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -18,7 +18,7 @@ func registerRoutes(r chi.Router) { }) r.Post("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { @@ -26,11 +26,11 @@ func registerRoutes(r chi.Router) { }) r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index dd8618f..0664466 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -17,21 +17,21 @@ endpoints.forEach(function(endpoint) { if (endpoint.hasOwnProperty('post')) { %> r.Post("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) <% } if (endpoint.hasOwnProperty('put')) { %> r.Put("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) <% } if (endpoint.hasOwnProperty('delete')) { %> r.Delete("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + w.WriteHeader(http.StatusNoContent) }) <% } From 97dc48a3ff922e080d2603d465faade534d4bf0a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 17:13:40 +0100 Subject: [PATCH 020/346] chore: output a newline when ListenAndServe failed --- examples/go/app.go | 2 +- src/templates/app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index c0e7552..88b5a62 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -11,7 +11,7 @@ func main() { fmt.Println("Listen on 3000") if err := http.ListenAndServe(":3000", r); err != nil { - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } } diff --git a/src/templates/app.go b/src/templates/app.go index c0e7552..88b5a62 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -11,7 +11,7 @@ func main() { fmt.Println("Listen on 3000") if err := http.ListenAndServe(":3000", r); err != nil { - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v", err) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } } From 08764b176b360a9d0b1b03ae2231c9a540c44aea Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 17:16:48 +0100 Subject: [PATCH 021/346] refactor: simplify ListenAndServe error handling From https://golang.org/pkg/net/http/#ListenAndServe: "ListenAndServe always returns a non-nil error." --- examples/go/app.go | 7 +++---- src/templates/app.go | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 88b5a62..7e64f4d 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -10,8 +10,7 @@ func main() { registerRoutes(r) fmt.Println("Listen on 3000") - if err := http.ListenAndServe(":3000", r); err != nil { - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) - os.Exit(1) - } + err := http.ListenAndServe(":3000", r) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) + os.Exit(1) } diff --git a/src/templates/app.go b/src/templates/app.go index 88b5a62..7e64f4d 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -10,8 +10,7 @@ func main() { registerRoutes(r) fmt.Println("Listen on 3000") - if err := http.ListenAndServe(":3000", r); err != nil { - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) - os.Exit(1) - } + err := http.ListenAndServe(":3000", r) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) + os.Exit(1) } From f50c4bf3864741e808ca43b7ad6dd73a1ddcb1ec Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 18:25:25 +0100 Subject: [PATCH 022/346] chore: don't send Content-Type header for 404 responses See for details: https://stackoverflow.com/questions/57399153/should-content-type-header-be-set-for-404-and-204-responses --- examples/js/routes.js | 6 +++--- src/templates/routes.js.ejs | 2 +- tests/crud.robot | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/js/routes.js b/examples/js/routes.js index 6d72fdc..28721c4 100644 --- a/examples/js/routes.js +++ b/examples/js/routes.js @@ -9,7 +9,7 @@ app.get('/v1/categories/count', (req, res) => { throw err } if (rows.length === 0) { - res.type('application/json').status(404).end() + res.status(404).end() return } res.json(rows[0]) @@ -26,7 +26,7 @@ app.get('/v1/collections/:collectionId/categories/count', (req, res) => { throw err } if (rows.length === 0) { - res.type('application/json').status(404).end() + res.status(404).end() return } res.json(rows[0]) @@ -68,7 +68,7 @@ app.get('/v1/categories/:categoryId', (req, res) => { throw err } if (rows.length === 0) { - res.type('application/json').status(404).end() + res.status(404).end() return } res.json(rows[0]) diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 4dedbd0..f470756 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -20,7 +20,7 @@ app.get('<%- path %>', (req, res) => { res.json(rows) <% } else { -%> if (rows.length === 0) { - res.type('application/json').status(404).end() + res.status(404).end() return } res.json(rows[0]) diff --git a/tests/crud.robot b/tests/crud.robot index ba7d75b..273682c 100644 --- a/tests/crud.robot +++ b/tests/crud.robot @@ -26,7 +26,6 @@ GET should return a value GET should return not found ${response}= Get Request api /v1/categories/100 Status Should Be 404 ${response} - Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 PUT should update an object &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 From adaeef133ae684d16cd9a323b779b3619af4ba4e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 19:32:58 +0100 Subject: [PATCH 023/346] refactor(tests): check for 404 after deletion --- tests/crud.robot | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/crud.robot b/tests/crud.robot index 273682c..b7316d3 100644 --- a/tests/crud.robot +++ b/tests/crud.robot @@ -23,9 +23,6 @@ GET should return a value Status Should Be 200 ${response} Dictionary Should Contain Item ${response.json()} counter 1 -GET should return not found - ${response}= Get Request api /v1/categories/100 - Status Should Be 404 ${response} PUT should update an object &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 @@ -33,9 +30,8 @@ PUT should update an object Status Should Be 204 ${response} DELETE should remove an object - ${response}= Delete Request api /v1/categories/1 - Status Should Be 204 ${response} + ${response}= Delete Request api /v1/categories/1 + Status Should Be 204 ${response} # checks that it was removed - ${response}= Get Request api /v1/categories/count - Status Should Be 200 ${response} - Dictionary Should Contain Item ${response.json()} counter 0 + ${response}= Get Request api /v1/categories/1 + Status Should Be 404 ${response} From bfde15bd18f84024978da1be3d1ec154b76f0fdd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 20:03:53 +0100 Subject: [PATCH 024/346] chore: improve tests for GET/PUT by inspecting response body --- tests/crud.robot | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/crud.robot b/tests/crud.robot index b7316d3..725c8cd 100644 --- a/tests/crud.robot +++ b/tests/crud.robot @@ -19,15 +19,26 @@ POST should create an object Dictionary Should Contain Item ${response.json()} counter 1 GET should return a value - ${response}= Get Request api /v1/categories/count - Status Should Be 200 ${response} - Dictionary Should Contain Item ${response.json()} counter 1 + ${response}= Get Request api /v1/categories/1 + ${body}= Set Variable ${response.json()} + Status Should Be 200 ${response} + Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 + &{expected}= Create Dictionary id=${1} name=Sport name_ru=${null} slug=sport + Dictionaries Should Be Equal ${body} ${expected} PUT should update an object - &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 - ${response}= Put Request api /v1/categories/1 json=${payload} - Status Should Be 204 ${response} + &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 + ${response}= Put Request api /v1/categories/1 json=${payload} + Status Should Be 204 ${response} + # checks that it was updated + ${response}= Get Request api /v1/categories/1 + ${body}= Set Variable ${response.json()} + Status Should Be 200 ${response} + Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 + Dictionary Should Contain Item ${body} name Fauna + Dictionary Should Contain Item ${body} name_ru Фауна + Dictionary Should Contain Item ${body} slug fauna DELETE should remove an object ${response}= Delete Request api /v1/categories/1 From 4baf098080b972474d3680ad8aadfe1153cc71c9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Aug 2020 20:13:08 +0100 Subject: [PATCH 025/346] test: check that GET /v1/categories return a list of values --- tests/crud.robot | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/crud.robot b/tests/crud.robot index 725c8cd..8f5eda2 100644 --- a/tests/crud.robot +++ b/tests/crud.robot @@ -26,6 +26,14 @@ GET should return a value &{expected}= Create Dictionary id=${1} name=Sport name_ru=${null} slug=sport Dictionaries Should Be Equal ${body} ${expected} +GET should return a list of values + ${response}= Get Request api /v1/categories + ${body}= Set Variable ${response.json()} + Status Should Be 200 ${response} + Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 + &{expected}= Create Dictionary id=${1} name=Sport name_ru=${null} slug=sport + Length Should Be ${body} 1 + Dictionaries Should Be Equal ${body[0]} ${expected} PUT should update an object &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 From 2daee7afd7b19aee8b765c4f12e82845cc524989 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 11:39:01 +0100 Subject: [PATCH 026/346] chore: fix inconsistent member names in POST/PUT and GET in examples Previously members for POST/PUT had camel case names (but persisted to database in a snake case) and GET returned them in a snake case. There were 2 ways to fix this: - GET should use "name_ru AS nameRu" in SQL query - POST/PUT should accept members in a snake case I used the 2nd approach. --- README.md | 8 ++++---- examples/js/endpoints.yaml | 10 +++++----- examples/js/routes.js | 8 ++++---- tests/crud.robot | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 47a4c84..32bbbeb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) FROM categories post: >- INSERT INTO categories(name, slug, created_at, created_by, updated_at, updated_by) - VALUES (:b.name, :b.slug, NOW(), :b.userId, NOW(), :b.userId) + VALUES (:b.name, :b.slug, NOW(), :b.user_id, NOW(), :b.user_id) - path: /v1/categories/:categoryId get: >- @@ -32,7 +32,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) WHERE id = :p.categoryId put: >- UPDATE categories - SET name = :b.name, name_ru = :b.nameRu, slug = :b.slug, updated_at = NOW(), updated_by = :b.userId + SET name = :b.name, name_ru = :b.name_ru, slug = :b.slug, updated_at = NOW(), updated_by = :b.user_id WHERE id = :p.categoryId delete: >- DELETE @@ -72,14 +72,14 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) ```console $ curl http://localhost:3000/v1/categories/count {"counter":0} - $ curl -i -H 'Content-Type: application/json' -d '{"name":"Sport","nameRu":"Спорт","slug":"sport","userId":100}' http://localhost:3000/v1/categories + $ curl -i -H 'Content-Type: application/json' -d '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:33 GMT Connection: keep-alive $ curl http://localhost:3000/v1/categories [{"id":1,"name":"Sport","name_ru":"Спорт","slug":"sport"}] - $ curl -i -H 'Content-Type: application/json' -d '{"name":"Fauna","nameRu":"Фауна","slug":"fauna","userId":101}' -X PUT http://localhost:3000/v1/categories/1 + $ curl -i -H 'Content-Type: application/json' -d '{"name":"Fauna","name_ru":"Фауна","slug":"fauna","user_id":101}' -X PUT http://localhost:3000/v1/categories/1 HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:34 GMT diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 6864eb6..06604a5 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -29,12 +29,12 @@ ) VALUES ( :b.name - , :b.nameRu + , :b.name_ru , :b.slug , NOW() - , :b.userId + , :b.user_id , NOW() - , :b.userId + , :b.user_id ) - path: /v1/categories/:categoryId @@ -48,10 +48,10 @@ put: >- UPDATE categories SET name = :b.name - , name_ru = :b.nameRu + , name_ru = :b.name_ru , slug = :b.slug , updated_at = NOW() - , updated_by = :b.userId + , updated_by = :b.user_id WHERE id = :p.categoryId delete: >- DELETE diff --git a/examples/js/routes.js b/examples/js/routes.js index 28721c4..a6d39a3 100644 --- a/examples/js/routes.js +++ b/examples/js/routes.js @@ -48,8 +48,8 @@ app.get('/v1/categories', (req, res) => { app.post('/v1/categories', (req, res) => { pool.query( - 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :nameRu , :slug , NOW() , :userId , NOW() , :userId )', - { "name": req.body.name, "nameRu": req.body.nameRu, "slug": req.body.slug, "userId": req.body.userId }, + 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { throw err @@ -78,8 +78,8 @@ app.get('/v1/categories/:categoryId', (req, res) => { app.put('/v1/categories/:categoryId', (req, res) => { pool.query( - 'UPDATE categories SET name = :name , name_ru = :nameRu , slug = :slug , updated_at = NOW() , updated_by = :userId WHERE id = :categoryId', - { "name": req.body.name, "nameRu": req.body.nameRu, "slug": req.body.slug, "userId": req.body.userId, "categoryId": req.params.categoryId }, + 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { throw err diff --git a/tests/crud.robot b/tests/crud.robot index 8f5eda2..90a58a0 100644 --- a/tests/crud.robot +++ b/tests/crud.robot @@ -10,7 +10,7 @@ ${SERVER_URL} http://host.docker.internal:3000 ** Test Cases *** POST should create an object - &{payload}= Create Dictionary name=Sport slug=sport userId=1 + &{payload}= Create Dictionary name=Sport slug=sport user_id=1 ${response}= Post Request api /v1/categories json=${payload} Status Should Be 204 ${response} # checks that it was created @@ -36,7 +36,7 @@ GET should return a list of values Dictionaries Should Be Equal ${body[0]} ${expected} PUT should update an object - &{payload}= Create Dictionary name=Fauna nameRu=Фауна slug=fauna userId=1 + &{payload}= Create Dictionary name=Fauna name_ru=Фауна slug=fauna user_id=1 ${response}= Put Request api /v1/categories/1 json=${payload} Status Should Be 204 ${response} # checks that it was updated From a6fde355983b73e93115545500c0e5501ea94023 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 13:25:27 +0100 Subject: [PATCH 027/346] chore(golang): add stubs in order to pass the tests without database Part of #9 --- examples/go/routes.go | 66 ++++++++++++++++++++++++++++++------- src/templates/routes.go.ejs | 61 ++++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 17 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 2e78305..3719e10 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -1,35 +1,79 @@ package main +import "encoding/json" +import "fmt" import "net/http" +import "strconv" import "github.com/go-chi/chi" +type Category struct { + Id int `json:"id"` + Name *string `json:"name"` + NameRu *string `json:"name_ru"` + Slug *string `json:"slug"` +} + func registerRoutes(r chi.Router) { + categories := make(map[int]Category) + cnt := 0 + + r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, `{"counter": %d}`, len(categories)) - r.Get("/v1/categories/count", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) }) - r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + category, exist := categories[id] + if !exist { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&category) + }) - r.Get("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + list := []Category{categories[1]} + json.NewEncoder(w).Encode(&list) + }) - r.Post("/v1/categories", func(w http.ResponseWriter, _ *http.Request) { + r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { + var category Category + json.NewDecoder(r.Body).Decode(&category) + cnt += 1 + category.Id = cnt + categories[cnt] = category w.WriteHeader(http.StatusNoContent) }) - r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) + r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + category, exist := categories[id] + if !exist { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&category) + }) - r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { + r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + var category Category + json.NewDecoder(r.Body).Decode(&category) + categories[id] = category w.WriteHeader(http.StatusNoContent) }) - r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, _ *http.Request) { + r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + delete(categories, id) w.WriteHeader(http.StatusNoContent) }) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 0664466..4be6be0 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -1,36 +1,85 @@ package main +import "encoding/json" +import "fmt" import "net/http" +import "strconv" import "github.com/go-chi/chi" +type Category struct { + Id int `json:"id"` + Name *string `json:"name"` + NameRu *string `json:"name_ru"` + Slug *string `json:"slug"` +} + func registerRoutes(r chi.Router) { + categories := make(map[int]Category) + cnt := 0 <% endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); - if (endpoint.hasOwnProperty('get') || endpoint.hasOwnProperty('get_list')) { + const hasGetOne = endpoint.hasOwnProperty('get'); + const hasGetMany = endpoint.hasOwnProperty('get_list'); + if (hasGetOne || hasGetMany) { +%> + r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { +<% + if (path === '/v1/categories/count') { +-%> + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, `{"counter": %d}`, len(categories)) +<% + } else if (hasGetMany) { +-%> + w.Header().Set("Content-Type", "application/json; charset=utf-8") + list := []Category{categories[1]} + json.NewEncoder(w).Encode(&list) +<% + } else { +-%> + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + category, exist := categories[id] + if !exist { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&category) +<% + } %> - r.Get("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("TODO")) }) <% } if (endpoint.hasOwnProperty('post')) { %> - r.Post("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + var category Category + json.NewDecoder(r.Body).Decode(&category) + cnt += 1 + category.Id = cnt + categories[cnt] = category w.WriteHeader(http.StatusNoContent) }) <% } if (endpoint.hasOwnProperty('put')) { %> - r.Put("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + var category Category + json.NewDecoder(r.Body).Decode(&category) + categories[id] = category w.WriteHeader(http.StatusNoContent) }) <% } if (endpoint.hasOwnProperty('delete')) { %> - r.Delete("<%- path %>", func(w http.ResponseWriter, _ *http.Request) { + r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) + delete(categories, id) w.WriteHeader(http.StatusNoContent) }) <% From 628913b155d896564911d19435eeff35433cdab1 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 13:44:13 +0100 Subject: [PATCH 028/346] chore: remove /v1/categories/count mapping from the example Keep it short by having only methods for CRUD operations. --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 32bbbeb..df347b1 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,6 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Create a mapping file `endpoints.yaml` ```console $ vim endpoints.yaml - - path: /v1/categories/count - get: SELECT COUNT(*) AS counter FROM categories - - path: /v1/categories get_list: >- SELECT id, name, name_ru, slug @@ -70,8 +67,6 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Test that it works ```console - $ curl http://localhost:3000/v1/categories/count - {"counter":0} $ curl -i -H 'Content-Type: application/json' -d '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" From 721f09c3b7daea60f6f4c961b72564aee996192f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 15:15:22 +0100 Subject: [PATCH 029/346] refactor: simplify regexp with lookbehind See https://www.stefanjudis.com/today-i-learned/the-complicated-syntax-of-lookaheads-in-javascript-regular-expressions/ --- src/cli.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/cli.js b/src/cli.js index cf23660..dc55dd6 100755 --- a/src/cli.js +++ b/src/cli.js @@ -46,7 +46,7 @@ const createApp = async (destDir, lang) => { 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, ':'); +const removePlaceholders = (query) => query.replace(/(?<=:)[pb]\./g, ''); // "/categories/:id" => "/categories/{id}" // (used only with Golang's go-chi) @@ -91,12 +91,7 @@ const createEndpoints = async (destDir, lang, config) => { "endpoints": config, // "... WHERE id = :p.id" => [ "p.id" ] => [ "p.id" ] - "extractParams": (query) => { - const params = query.match(/:[pb]\.\w+/g) || []; - return params.length > 0 - ? params.map(p => p.substring(1)) - : params; - }, + "extractParams": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], // [ "p.page", "b.num" ] => '{ "page" : req.params.page, "num": req.body.num }' "formatParams": (params) => { From 62f8d76afdf26ee30aab9f59353178ec2d95ed3f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 15:23:03 +0100 Subject: [PATCH 030/346] refactor: simplify regexp with a captured group --- src/cli.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cli.js b/src/cli.js index dc55dd6..d736106 100755 --- a/src/cli.js +++ b/src/cli.js @@ -50,9 +50,7 @@ const removePlaceholders = (query) => query.replace(/(?<=:)[pb]\./g, ''); // "/categories/:id" => "/categories/{id}" // (used only with Golang's go-chi) -const convertPathPlaceholders = (path) => { - return path.replace(/:[^\/]+/g, (placeholder) => '{' + placeholder.slice(1) + '}'); -}; +const convertPathPlaceholders = (path) => path.replace(/:([^\/]+)/g, '{$1}'); const createEndpoints = async (destDir, lang, config) => { const fileName = `routes.${lang}` From ebac727368f9142989081f2d1268eab77e384298 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 15 Aug 2020 16:08:42 +0100 Subject: [PATCH 031/346] chore(golang): respect PORT env variable Part of #9 --- examples/go/app.go | 9 +++++++-- src/templates/app.go | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 7e64f4d..3fcdfb3 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -9,8 +9,13 @@ func main() { r := chi.NewRouter() registerRoutes(r) - fmt.Println("Listen on 3000") - err := http.ListenAndServe(":3000", r) + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + fmt.Println("Listen on " + port) + err := http.ListenAndServe(":"+port, r) fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } diff --git a/src/templates/app.go b/src/templates/app.go index 7e64f4d..3fcdfb3 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -9,8 +9,13 @@ func main() { r := chi.NewRouter() registerRoutes(r) - fmt.Println("Listen on 3000") - err := http.ListenAndServe(":3000", r) + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + fmt.Println("Listen on " + port) + err := http.ListenAndServe(":"+port, r) fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } From 9dfae6b864b95f75125c8cacc307c88ce4764fed Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 22 Aug 2020 20:31:52 +0100 Subject: [PATCH 032/346] chore(golang): generate DTOs based on queries Part of #9 --- examples/go/routes.go | 18 +++++ package-lock.json | 13 ++++ package.json | 3 +- src/cli.js | 10 +++ src/templates/routes.go.ejs | 151 ++++++++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 3719e10..e64873f 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -13,6 +13,24 @@ type Category struct { Slug *string `json:"slug"` } +type Dto1 struct { + Counter string `json:"counter"` +} + +type Dto3 struct { + Id string `json:"id"` + Name string `json:"name"` + NameRu string `json:"nameRu"` + Slug string `json:"slug"` +} + +type Dto4 struct { + Name string `json:"name"` + NameRu string `json:"nameRu"` + Slug string `json:"slug"` + UserId string `json:"userId"` +} + func registerRoutes(r chi.Router) { categories := make(map[int]Category) cnt := 0 diff --git a/package-lock.json b/package-lock.json index b4215f1..5b0299e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -131,6 +136,14 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "node-sql-parser": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-3.0.4.tgz", + "integrity": "sha512-ANB/paC3ZdcvrbRdiuQFsTvrmgzPozDUueTivO0Iep82h6KVRoER2h/KeZPsG0umYtCzwGY6KNhvB8xtJ6B/Rw==", + "requires": { + "big-integer": "^1.6.48" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 1e0262f..2e8421c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "ejs": "~3.1.3", "js-yaml": "~3.14.0", - "minimist": "~1.2.5" + "minimist": "~1.2.5", + "node-sql-parser": "~3.0.4" } } diff --git a/src/cli.js b/src/cli.js index d736106..43a414f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -7,6 +7,8 @@ const path = require('path'); const parseArgs = require('minimist'); +const { Parser } = require('node-sql-parser'); + const endpointsFile = 'endpoints.yaml'; const parseCommandLineArgs = (args) => { @@ -83,6 +85,8 @@ const createEndpoints = async (destDir, lang, config) => { 'b': 'req.body' } + const parser = new Parser(); + const resultedCode = await ejs.renderFile( `${__dirname}/templates/routes.${lang}.ejs`, { @@ -105,6 +109,12 @@ const createEndpoints = async (destDir, lang, config) => { // (used only with Golang's go-chi) "convertPathPlaceholders": convertPathPlaceholders, + + // (used only with Golang) + "sqlParser": parser, + + // (used only with Golang) + "removePlaceholders": removePlaceholders, } ); diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 4be6be0..a5ad351 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -13,6 +13,157 @@ type Category struct { Slug *string `json:"slug"` } +<% +// {'columns': +// [ +// { +// expr: { type: 'column_ref', table: null, column: 'name_ru' }, +// as: 'nameRu' +// } +// ] +// } => [ 'nameRu' ] +function extractSelectParameters(queryAst) { + return queryAst.columns + .map(column => column.as !== null ? column.as : column.expr.column); +} + +// {'values': +// [ +// { +// type: 'expr_list', +// value: [ { type: 'param', value: 'user_id' } ] +// } +// ] +// } => [ 'user_id' ] +function extractInsertValues(queryAst) { + const values = queryAst.values.flatMap(elem => elem.value) + .map(elem => elem.type === 'param' ? elem.value : null) + .filter(elem => elem); // filter out nulls + return Array.from(new Set(values)); +} + +// {'set': +// [ +// { +// column: 'updated_by', +// value: { type: 'param', value: 'user_id' }, +// table: null +// } +// ] +// } => [ 'user_id' ] +function extractUpdateValues(queryAst) { + // TODO: distinguish between b.param and q.param and extract only first + return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null) + .filter(value => value) // filter out nulls +} + +function extractProperties(queryAst) { + if (queryAst.type === 'select') { + return extractSelectParameters(queryAst); + } + + if (queryAst.type === 'insert') { + return extractInsertValues(queryAst); + } + + if (queryAst.type === 'update') { + return extractUpdateValues(queryAst); + } + + return []; +} + +function addTypes(props) { + return props.map(prop => { + return { + "name": prop, + // TODO: resolve/autoguess types + "type": "string" + } + }); +} + +function query2dto(parser, query) { + const queryAst = parser.astify(query); + const props = extractProperties(queryAst).map(snake2camelCase); + if (props.length === 0) { + console.warn('Could not create DTO for query:', formatQuery(query)); + console.debug('Query AST:'); + console.debug(queryAst); + return null; + } + const propsWithTypes = addTypes(props); + return { + // TODO: assign DTO name dynamically + "name": "Dto" + ++globalDtoCounter, + "props": propsWithTypes, + // max length is needed for proper formatting + "maxFieldNameLength": lengthOfLongestString(props), + // required for de-duplication + // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" + // TODO: sort before join + "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') + }; +} + +// "nameRu" => "NameRu" +function capitalize(str) { + return str[0].toUpperCase() + str.slice(1); +} + +// "name_ru" => "nameRu" +function snake2camelCase(str) { + return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase()); +} + +// ["a", "bb", "ccc"] => 3 +function lengthOfLongestString(arr) { + return arr + .map(el => el.length) + .reduce( + (acc, val) => val > acc ? val : acc, + 0 /* initial value */ + ); +} + +function dto2struct(dto) { + let result = `type ${dto.name} struct {\n`; + dto.props.forEach(prop => { + const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength); + result += `\t${fieldName} ${prop.type} \`json:"${prop.name}"\`\n` + }); + result += '}\n'; + + return result; +} + +let globalDtoCounter = 0; + +const dtoCache = {}; +function cacheDto(dto) { + dtoCache[dto.signature] = dto.name; + return dto; +} +function dtoInCache(dto) { + return dtoCache.hasOwnProperty(dto.signature); +} + +const verbs_with_dto = [ 'get', 'get_list', 'post', 'put' ] +endpoints.forEach(function(endpoint) { + const dtos = Object.keys(endpoint) + .filter(propName => verbs_with_dto.includes(propName)) + .map(propName => endpoint[propName]) + .map(query => query2dto(sqlParser, removePlaceholders(query))) + .filter(elem => elem) // filter out nulls + .filter(dto => !dtoInCache(dto)) + .map(dto => dto2struct(cacheDto(dto))) + .forEach(struct => { +-%> +<%- struct %> +<% + }); +}); +-%> func registerRoutes(r chi.Router) { categories := make(map[int]Category) cnt := 0 From cb3be9f48e8ddcf419478666898ad586f9e4006d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 22 Aug 2020 20:34:06 +0100 Subject: [PATCH 033/346] chore: add a comment --- src/cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.js b/src/cli.js index 43a414f..70d9255 100755 --- a/src/cli.js +++ b/src/cli.js @@ -96,6 +96,7 @@ const createEndpoints = async (destDir, lang, config) => { "extractParams": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], // [ "p.page", "b.num" ] => '{ "page" : req.params.page, "num": req.body.num }' + // (used only with Express) "formatParams": (params) => { return params.length > 0 ? '{ ' + Array.from(new Set(params), p => `"${p.substring(2)}": ${placeholdersMap[p.substring(0, 1)]}.${p.substring(2)}`).join(', ') + ' }' From 425a720b24af68544334d6f259d8c0d7db58f174 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 23 Aug 2020 18:23:07 +0100 Subject: [PATCH 034/346] chore(golang): add code for making a database connection Part of #9 --- examples/go/app.go | 34 +++++++++++++++++++++++++++++++++- examples/go/go.mod | 5 ++++- src/templates/app.go | 34 +++++++++++++++++++++++++++++++++- src/templates/go.mod.ejs | 5 ++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 3fcdfb3..49b3dc1 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -1,11 +1,43 @@ package main +import "database/sql" import "fmt" import "net/http" import "os" import "github.com/go-chi/chi" +import _ "github.com/go-sql-driver/mysql" + func main() { + mapper := func(name string) string { + value := os.Getenv(name) + switch name { + case "DB_HOST": + if value == "" { + value = "localhost" + } + case "DB_NAME", "DB_USER", "DB_PASSWORD": + if value == "" { + fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) + os.Exit(1) + } + } + return value + } + + dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) + db, err := sql.Open("mysql", dsn) + if err != nil { + fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err) + os.Exit(1) + } + defer db.Close() + + if err = db.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "Could not connect to database: %v\n", err) + os.Exit(1) + } + r := chi.NewRouter() registerRoutes(r) @@ -15,7 +47,7 @@ func main() { } fmt.Println("Listen on " + port) - err := http.ListenAndServe(":"+port, r) + err = http.ListenAndServe(":"+port, r) fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } diff --git a/examples/go/go.mod b/examples/go/go.mod index f8e94a3..0c83f94 100644 --- a/examples/go/go.mod +++ b/examples/go/go.mod @@ -2,4 +2,7 @@ module main go 1.14 -require github.com/go-chi/chi v4.1.2+incompatible +require ( + github.com/go-chi/chi v4.1.2+incompatible + github.com/go-sql-driver/mysql v1.5.0 +) diff --git a/src/templates/app.go b/src/templates/app.go index 3fcdfb3..49b3dc1 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -1,11 +1,43 @@ package main +import "database/sql" import "fmt" import "net/http" import "os" import "github.com/go-chi/chi" +import _ "github.com/go-sql-driver/mysql" + func main() { + mapper := func(name string) string { + value := os.Getenv(name) + switch name { + case "DB_HOST": + if value == "" { + value = "localhost" + } + case "DB_NAME", "DB_USER", "DB_PASSWORD": + if value == "" { + fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) + os.Exit(1) + } + } + return value + } + + dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) + db, err := sql.Open("mysql", dsn) + if err != nil { + fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err) + os.Exit(1) + } + defer db.Close() + + if err = db.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "Could not connect to database: %v\n", err) + os.Exit(1) + } + r := chi.NewRouter() registerRoutes(r) @@ -15,7 +47,7 @@ func main() { } fmt.Println("Listen on " + port) - err := http.ListenAndServe(":"+port, r) + err = http.ListenAndServe(":"+port, r) fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) os.Exit(1) } diff --git a/src/templates/go.mod.ejs b/src/templates/go.mod.ejs index f8e94a3..0c83f94 100644 --- a/src/templates/go.mod.ejs +++ b/src/templates/go.mod.ejs @@ -2,4 +2,7 @@ module main go 1.14 -require github.com/go-chi/chi v4.1.2+incompatible +require ( + github.com/go-chi/chi v4.1.2+incompatible + github.com/go-sql-driver/mysql v1.5.0 +) From aff164e53dadcc3388eaea3a6c0591c2c2b09eeb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 24 Aug 2020 16:43:12 +0100 Subject: [PATCH 035/346] chore: add TODO item --- src/templates/routes.go.ejs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index a5ad351..858108b 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -57,6 +57,7 @@ function extractUpdateValues(queryAst) { .filter(value => value) // filter out nulls } +// TODO: consider taking into account b.params from WHERE clause function extractProperties(queryAst) { if (queryAst.type === 'select') { return extractSelectParameters(queryAst); From b86ca3365359204fe9663450b783d8a684459293 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 24 Aug 2020 16:45:33 +0100 Subject: [PATCH 036/346] refactor: modify a syntax for specifying queries in YAML BREAKING CHANGE: Old syntax: - path: /v1/categories/count get: select count(*) as counter from categories New syntax: - path: /v1/categories/count get: query: select count(*) as counter from categories The new syntax is needed for upcoming support of DTO objects. This change also restructures internal representation in order to simplify code. Part of #9 --- README.md | 41 +++++++------ examples/js/endpoints.yaml | 111 +++++++++++++++++++----------------- src/cli.js | 52 ++++++++++++----- src/templates/routes.go.ejs | 39 +++++++------ src/templates/routes.js.ejs | 46 ++++++++------- 5 files changed, 166 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index df347b1..32004e1 100644 --- a/README.md +++ b/README.md @@ -15,26 +15,31 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) ```console $ vim endpoints.yaml - path: /v1/categories - get_list: >- - SELECT id, name, name_ru, slug - FROM categories - post: >- - INSERT INTO categories(name, slug, created_at, created_by, updated_at, updated_by) - VALUES (:b.name, :b.slug, NOW(), :b.user_id, NOW(), :b.user_id) + get_list: + query: >- + SELECT id, name, name_ru, slug + FROM categories + post: + query: >- + INSERT INTO categories(name, slug, created_at, created_by, updated_at, updated_by) + VALUES (:b.name, :b.slug, NOW(), :b.user_id, NOW(), :b.user_id) - path: /v1/categories/:categoryId - get: >- - SELECT id, name, name_ru, slug - FROM categories - WHERE id = :p.categoryId - put: >- - UPDATE categories - SET name = :b.name, name_ru = :b.name_ru, slug = :b.slug, updated_at = NOW(), updated_by = :b.user_id - WHERE id = :p.categoryId - delete: >- - DELETE - FROM categories - WHERE id = :p.categoryId + get: + query: >- + SELECT id, name, name_ru, slug + FROM categories + WHERE id = :p.categoryId + put: + query: >- + UPDATE categories + SET name = :b.name, name_ru = :b.name_ru, slug = :b.slug, updated_at = NOW(), updated_by = :b.user_id + WHERE id = :p.categoryId + delete: + query: >- + DELETE + FROM categories + WHERE id = :p.categoryId ``` Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 06604a5..6e4f4c7 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -1,59 +1,66 @@ - path: /v1/categories/count - get: SELECT COUNT(*) AS counter FROM categories + get: + query: SELECT COUNT(*) AS counter FROM categories - path: /v1/collections/:collectionId/categories/count - get: >- - 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 = :p.collectionId + get: + query: >- + 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 = :p.collectionId - path: /v1/categories - get_list: >- - SELECT id - , name - , name_ru - , slug - FROM categories - post: >- - INSERT - INTO categories - ( name - , name_ru - , slug - , created_at - , created_by - , updated_at - , updated_by - ) - VALUES - ( :b.name - , :b.name_ru - , :b.slug - , NOW() - , :b.user_id - , NOW() - , :b.user_id - ) + get_list: + query: >- + SELECT id + , name + , name_ru + , slug + FROM categories + post: + query: >- + INSERT + INTO categories + ( name + , name_ru + , slug + , created_at + , created_by + , updated_at + , updated_by + ) + VALUES + ( :b.name + , :b.name_ru + , :b.slug + , NOW() + , :b.user_id + , NOW() + , :b.user_id + ) - path: /v1/categories/:categoryId - get: >- - SELECT id - , name - , name_ru - , slug - FROM categories - WHERE id = :p.categoryId - put: >- - UPDATE categories - SET name = :b.name - , name_ru = :b.name_ru - , slug = :b.slug - , updated_at = NOW() - , updated_by = :b.user_id - WHERE id = :p.categoryId - delete: >- - DELETE - FROM categories - WHERE id = :p.categoryId + get: + query: >- + SELECT id + , name + , name_ru + , slug + FROM categories + WHERE id = :p.categoryId + put: + query: >- + UPDATE categories + SET name = :b.name + , name_ru = :b.name_ru + , slug = :b.slug + , updated_at = NOW() + , updated_by = :b.user_id + WHERE id = :p.categoryId + delete: + query: >- + DELETE + FROM categories + WHERE id = :p.categoryId diff --git a/src/cli.js b/src/cli.js index 70d9255..d20bf2d 100755 --- a/src/cli.js +++ b/src/cli.js @@ -23,11 +23,43 @@ const parseCommandLineArgs = (args) => { return argv; } +// Restructure YAML configuration to simplify downstream code. +// +// Converts +// { +// get_list: { query: }, +// put: { query: } +// } +// into +// { +// methods: [ +// { name: get_list, verb: get, query: }, +// { name: put, verb: put, query: } +// ] +// } +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) { @@ -64,20 +96,12 @@ const createEndpoints = async (destDir, lang, config) => { if (lang === 'go') { path = convertPathPlaceholders(path) } - if (endpoint.hasOwnProperty('get')) { - console.log('GET', path, '=>', removePlaceholders(flattenQuery(endpoint.get))); - } else if (endpoint.hasOwnProperty('get_list')) { - console.log('GET', path, '=>', removePlaceholders(flattenQuery(endpoint.get_list))); - } - if (endpoint.hasOwnProperty('post')) { - console.log('POST', path, '=>', removePlaceholders(flattenQuery(endpoint.post))); - } - if (endpoint.hasOwnProperty('put')) { - console.log('PUT', path, '=>', removePlaceholders(flattenQuery(endpoint.put))); - } - if (endpoint.hasOwnProperty('delete')) { - console.log('DELETE', path, '=>', removePlaceholders(flattenQuery(endpoint.delete))); - } + endpoint.methods.forEach(method => { + const sql = removePlaceholders(flattenQuery(method.query)); + const verb = method.verb.toUpperCase(); + + console.log(`${verb} ${path} => ${sql}`); + }); } const placeholdersMap = { diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 858108b..e97240d 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -149,11 +149,11 @@ function dtoInCache(dto) { return dtoCache.hasOwnProperty(dto.signature); } -const verbs_with_dto = [ 'get', 'get_list', 'post', 'put' ] +const verbs_with_dto = [ 'get', 'post', 'put' ] endpoints.forEach(function(endpoint) { - const dtos = Object.keys(endpoint) - .filter(propName => verbs_with_dto.includes(propName)) - .map(propName => endpoint[propName]) + const dtos = endpoint.methods + .filter(method => verbs_with_dto.includes(method.verb)) + .map(method => method.query) .map(query => query2dto(sqlParser, removePlaceholders(query))) .filter(elem => elem) // filter out nulls .filter(dto => !dtoInCache(dto)) @@ -171,24 +171,26 @@ func registerRoutes(r chi.Router) { <% endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); - const hasGetOne = endpoint.hasOwnProperty('get'); - const hasGetMany = endpoint.hasOwnProperty('get_list'); - if (hasGetOne || hasGetMany) { + + endpoint.methods.forEach(function(method) { + const hasGetOne = method.name === 'get'; + const hasGetMany = method.name === 'get_list'; + if (hasGetOne || hasGetMany) { %> r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { <% - if (path === '/v1/categories/count') { + if (path === '/v1/categories/count') { -%> w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, `{"counter": %d}`, len(categories)) <% - } else if (hasGetMany) { + } else if (hasGetMany) { -%> w.Header().Set("Content-Type", "application/json; charset=utf-8") list := []Category{categories[1]} json.NewEncoder(w).Encode(&list) <% - } else { + } else { -%> id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) category, exist := categories[id] @@ -199,12 +201,12 @@ endpoints.forEach(function(endpoint) { w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(&category) <% - } + } %> }) <% - } - if (endpoint.hasOwnProperty('post')) { + } + if (method.name === 'post') { %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var category Category @@ -215,8 +217,8 @@ endpoints.forEach(function(endpoint) { w.WriteHeader(http.StatusNoContent) }) <% - } - if (endpoint.hasOwnProperty('put')) { + } + if (method.name === 'put') { %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) @@ -226,8 +228,8 @@ endpoints.forEach(function(endpoint) { w.WriteHeader(http.StatusNoContent) }) <% - } - if (endpoint.hasOwnProperty('delete')) { + } + if (method.name === 'delete') { %> r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) @@ -235,7 +237,8 @@ endpoints.forEach(function(endpoint) { w.WriteHeader(http.StatusNoContent) }) <% - } + } + }); }) %> } diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index f470756..93f678d 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -3,11 +3,14 @@ const register = (app, pool) => { <% endpoints.forEach(function(endpoint) { const path = endpoint.path; - const hasGetOne = endpoint.hasOwnProperty('get'); - const hasGetMany = endpoint.hasOwnProperty('get_list'); - if (hasGetOne || hasGetMany) { - const sql = hasGetOne ? endpoint.get : endpoint.get_list; - const params = extractParams(sql); + + endpoint.methods.forEach(function(method) { + const hasGetOne = method.name === 'get'; + const hasGetMany = method.name === 'get_list'; + + if (hasGetOne || hasGetMany) { + const sql = method.query; + const params = extractParams(sql); %> app.get('<%- path %>', (req, res) => { pool.query( @@ -16,26 +19,26 @@ app.get('<%- path %>', (req, res) => { if (err) { throw err } -<% if (hasGetMany) { -%> +<% if (hasGetMany) { -%> res.json(rows) -<% } else { -%> +<% } else { -%> if (rows.length === 0) { res.status(404).end() return } res.json(rows[0]) -<% } -%> +<% } -%> } ) }) <% - } - if (endpoint.hasOwnProperty('post')) { - const params = extractParams(endpoint.post); + } + if (method.name === 'post') { + const params = extractParams(method.query); %> app.post('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(endpoint.post) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { if (err) { throw err @@ -45,13 +48,13 @@ app.post('<%- path %>', (req, res) => { ) }) <% - } - if (endpoint.hasOwnProperty('put')) { - const params = extractParams(endpoint.put); + } + if (method.name === 'put') { + const params = extractParams(method.query); %> app.put('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(endpoint.put) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { if (err) { throw err @@ -61,13 +64,13 @@ app.put('<%- path %>', (req, res) => { ) }) <% - } - if (endpoint.hasOwnProperty('delete')) { - const params = extractParams(endpoint.delete); + } + if (method.name === 'delete') { + const params = extractParams(method.query); %> app.delete('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(endpoint.delete) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> (err, rows, fields) => { if (err) { throw err @@ -77,7 +80,8 @@ app.delete('<%- path %>', (req, res) => { ) }) <% - } + } + }); }); %> From 2c8d3545fb9bed578cab75e7246df75c4cac04fd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 24 Aug 2020 16:59:31 +0100 Subject: [PATCH 037/346] refactor: extract variables --- src/templates/routes.js.ejs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 93f678d..c1091ed 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -7,14 +7,17 @@ endpoints.forEach(function(endpoint) { endpoint.methods.forEach(function(method) { const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; + const sql = formatQuery(method.query); + const params = extractParams(method.query); + const formattedParams = params.length > 0 + ? '\n ' + formatParams(params) + ',' + : '' if (hasGetOne || hasGetMany) { - const sql = method.query; - const params = extractParams(sql); %> app.get('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(sql) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -34,11 +37,10 @@ app.get('<%- path %>', (req, res) => { <% } if (method.name === 'post') { - const params = extractParams(method.query); %> app.post('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -50,11 +52,10 @@ app.post('<%- path %>', (req, res) => { <% } if (method.name === 'put') { - const params = extractParams(method.query); %> app.put('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -66,11 +67,10 @@ app.put('<%- path %>', (req, res) => { <% } if (method.name === 'delete') { - const params = extractParams(method.query); %> app.delete('<%- path %>', (req, res) => { pool.query( - <%- formatQuery(method.query) %>,<%- params.length > 0 ? '\n ' + formatParams(params) + ',' : '' %> + <%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { throw err From 856eb99ac789a006658332efe3de171508635157 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 25 Aug 2020 18:08:19 +0100 Subject: [PATCH 038/346] refactor: formatQuery() now doesn't quote its parameter --- src/cli.js | 4 ++-- src/templates/routes.js.ejs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli.js b/src/cli.js index d20bf2d..ac9f4ea 100755 --- a/src/cli.js +++ b/src/cli.js @@ -127,9 +127,9 @@ const createEndpoints = async (destDir, lang, config) => { : params; }, - // "SELECT *\n FROM foo" => "'SELECT * FROM foo'" + // "SELECT *\n FROM foo" => "SELECT * FROM foo" "formatQuery": (query) => { - return "'" + removePlaceholders(flattenQuery(query)) + "'"; + return removePlaceholders(flattenQuery(query)); }, // (used only with Golang's go-chi) diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index c1091ed..3a1abc1 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -17,7 +17,7 @@ endpoints.forEach(function(endpoint) { %> app.get('<%- path %>', (req, res) => { pool.query( - <%- sql %>,<%- formattedParams %> + '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -40,7 +40,7 @@ app.get('<%- path %>', (req, res) => { %> app.post('<%- path %>', (req, res) => { pool.query( - <%- sql %>,<%- formattedParams %> + '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -55,7 +55,7 @@ app.post('<%- path %>', (req, res) => { %> app.put('<%- path %>', (req, res) => { pool.query( - <%- sql %>,<%- formattedParams %> + '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { throw err @@ -70,7 +70,7 @@ app.put('<%- path %>', (req, res) => { %> app.delete('<%- path %>', (req, res) => { pool.query( - <%- sql %>,<%- formattedParams %> + '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { throw err From 890073b8bfea3e75dbc5ba976973615456ab5bc0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 25 Aug 2020 18:40:42 +0100 Subject: [PATCH 039/346] refactor: rename a function --- src/cli.js | 2 +- src/templates/routes.js.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index ac9f4ea..db77dac 100755 --- a/src/cli.js +++ b/src/cli.js @@ -121,7 +121,7 @@ const createEndpoints = async (destDir, lang, config) => { // [ "p.page", "b.num" ] => '{ "page" : req.params.page, "num": req.body.num }' // (used only with Express) - "formatParams": (params) => { + "formatParamsAsJavaScriptObject": (params) => { return params.length > 0 ? '{ ' + Array.from(new Set(params), p => `"${p.substring(2)}": ${placeholdersMap[p.substring(0, 1)]}.${p.substring(2)}`).join(', ') + ' }' : params; diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 3a1abc1..8155cda 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -10,7 +10,7 @@ endpoints.forEach(function(endpoint) { const sql = formatQuery(method.query); const params = extractParams(method.query); const formattedParams = params.length > 0 - ? '\n ' + formatParams(params) + ',' + ? '\n ' + formatParamsAsJavaScriptObject(params) + ',' : '' if (hasGetOne || hasGetMany) { From f3235fbec64fca8a2f0d2acd7d24c63abed62dc9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 25 Aug 2020 19:22:12 +0100 Subject: [PATCH 040/346] refactor: perform query unification inside query2dto() --- src/templates/routes.go.ejs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index e97240d..f6d3444 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -85,6 +85,7 @@ function addTypes(props) { } function query2dto(parser, query) { + query = removePlaceholders(query); const queryAst = parser.astify(query); const props = extractProperties(queryAst).map(snake2camelCase); if (props.length === 0) { @@ -154,7 +155,7 @@ endpoints.forEach(function(endpoint) { const dtos = endpoint.methods .filter(method => verbs_with_dto.includes(method.verb)) .map(method => method.query) - .map(query => query2dto(sqlParser, removePlaceholders(query))) + .map(query => query2dto(sqlParser, query)) .filter(elem => elem) // filter out nulls .filter(dto => !dtoInCache(dto)) .map(dto => dto2struct(cacheDto(dto))) From 7cacd682d75ce51e49e127a9649ae4dd9db0b6dd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 25 Aug 2020 23:33:54 +0100 Subject: [PATCH 041/346] chore(golang): include function name into error message --- examples/go/app.go | 2 +- src/templates/app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 49b3dc1..68ec02a 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -34,7 +34,7 @@ func main() { defer db.Close() if err = db.Ping(); err != nil { - fmt.Fprintf(os.Stderr, "Could not connect to database: %v\n", err) + fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) os.Exit(1) } diff --git a/src/templates/app.go b/src/templates/app.go index 49b3dc1..68ec02a 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -34,7 +34,7 @@ func main() { defer db.Close() if err = db.Ping(); err != nil { - fmt.Fprintf(os.Stderr, "Could not connect to database: %v\n", err) + fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) os.Exit(1) } From 793e4fb3f91c03e325d376e56a9cf460df6de9f3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 26 Aug 2020 21:35:46 +0100 Subject: [PATCH 042/346] 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: https://github.com/go-sql-driver/mysql/issues/561#issuecomment-337441108 See also: - http://go-database-sql.org - https://github.com/jmoiron/sqlx - http://jmoiron.github.io/sqlx/ Part of #9 --- examples/go/app.go | 8 +-- examples/go/go.mod | 1 + examples/go/routes.go | 112 +++++++++++++++++++++++++----------- src/cli.js | 10 ++++ src/templates/app.go | 8 +-- src/templates/go.mod.ejs | 1 + src/templates/routes.go.ejs | 79 +++++++++++++++---------- 7 files changed, 148 insertions(+), 71 deletions(-) diff --git a/examples/go/app.go b/examples/go/app.go index 68ec02a..dd4d946 100644 --- a/examples/go/app.go +++ b/examples/go/app.go @@ -1,10 +1,10 @@ package main -import "database/sql" import "fmt" import "net/http" import "os" import "github.com/go-chi/chi" +import "github.com/jmoiron/sqlx" import _ "github.com/go-sql-driver/mysql" @@ -26,9 +26,9 @@ func main() { } dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) - db, err := sql.Open("mysql", dsn) + db, err := sqlx.Open("mysql", dsn) if err != nil { - fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err) + fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) os.Exit(1) } defer db.Close() @@ -39,7 +39,7 @@ func main() { } r := chi.NewRouter() - registerRoutes(r) + registerRoutes(r, db) port := os.Getenv("PORT") if port == "" { diff --git a/examples/go/go.mod b/examples/go/go.mod index 0c83f94..0df3f33 100644 --- a/examples/go/go.mod +++ b/examples/go/go.mod @@ -5,4 +5,5 @@ go 1.14 require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-sql-driver/mysql v1.5.0 + github.com/jmoiron/sqlx v1.2.0 ) diff --git a/examples/go/routes.go b/examples/go/routes.go index e64873f..94dd387 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -1,63 +1,96 @@ package main +import "database/sql" import "encoding/json" import "fmt" import "net/http" +import "os" import "strconv" import "github.com/go-chi/chi" +import "github.com/jmoiron/sqlx" type Category struct { - Id int `json:"id"` - Name *string `json:"name"` - NameRu *string `json:"name_ru"` - Slug *string `json:"slug"` + Id int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + NameRu *string `json:"name_ru" db:"name_ru"` + Slug string `json:"slug" db:"slug"` } type Dto1 struct { - Counter string `json:"counter"` + Counter *string `json:"counter,omitempty" db:"counter"` } type Dto3 struct { - Id string `json:"id"` - Name string `json:"name"` - NameRu string `json:"nameRu"` - Slug string `json:"slug"` + Id *string `json:"id,omitempty" db:"id"` + Name *string `json:"name,omitempty" db:"name"` + NameRu *string `json:"name_ru,omitempty" db:"name_ru"` + Slug *string `json:"slug,omitempty" db:"slug"` } type Dto4 struct { - Name string `json:"name"` - NameRu string `json:"nameRu"` - Slug string `json:"slug"` - UserId string `json:"userId"` + Name *string `json:"name,omitempty" db:"name"` + NameRu *string `json:"name_ru,omitempty" db:"name_ru"` + Slug *string `json:"slug,omitempty" db:"slug"` + UserId *string `json:"user_id,omitempty" db:"user_id"` } -func registerRoutes(r chi.Router) { +func registerRoutes(r chi.Router, db *sqlx.DB) { categories := make(map[int]Category) cnt := 0 r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - fmt.Fprintf(w, `{"counter": %d}`, len(categories)) - + var result Dto1 + err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories") + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } }) r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - category, exist := categories[id] - if !exist { - w.WriteHeader(http.StatusNotFound) + 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") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - json.NewEncoder(w).Encode(&category) + var result Dto1 + args := map[string]interface{}{ + "collectionId": chi.URLParam(r, "collectionId"), + } + err = nstmt.Get(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } }) r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - list := []Category{categories[1]} - json.NewEncoder(w).Encode(&list) - + var result []Dto3 + err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories") + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } }) r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { @@ -70,15 +103,28 @@ func registerRoutes(r chi.Router) { }) r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - category, exist := categories[id] - if !exist { - w.WriteHeader(http.StatusNotFound) + nstmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - json.NewEncoder(w).Encode(&category) + var result Dto3 + args := map[string]interface{}{ + "categoryId": chi.URLParam(r, "categoryId"), + } + err = nstmt.Get(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } }) r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { diff --git a/src/cli.js b/src/cli.js index db77dac..ed8cb02 100755 --- a/src/cli.js +++ b/src/cli.js @@ -127,6 +127,16 @@ const createEndpoints = async (destDir, lang, config) => { : params; }, + // [ "p.page", "b.num" ] => 'chi.URLParam(r, "page"), chi.URLParam(r, "num")' + // (used only with Golang's go-chi) + // TODO: do we need to de-deduplicate (new Set(params))? + // TODO: handle b.params + "formatParamsAsGolangVararg": (params) => { + return params.length > 0 + ? Array.from(params, p => `chi.URLParam(r, "${p.substring(2)}")`).join(', ') + : params; + }, + // "SELECT *\n FROM foo" => "SELECT * FROM foo" "formatQuery": (query) => { return removePlaceholders(flattenQuery(query)); diff --git a/src/templates/app.go b/src/templates/app.go index 68ec02a..dd4d946 100644 --- a/src/templates/app.go +++ b/src/templates/app.go @@ -1,10 +1,10 @@ package main -import "database/sql" import "fmt" import "net/http" import "os" import "github.com/go-chi/chi" +import "github.com/jmoiron/sqlx" import _ "github.com/go-sql-driver/mysql" @@ -26,9 +26,9 @@ func main() { } dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) - db, err := sql.Open("mysql", dsn) + db, err := sqlx.Open("mysql", dsn) if err != nil { - fmt.Fprintf(os.Stderr, "sql.Open failed: %v\n", err) + fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) os.Exit(1) } defer db.Close() @@ -39,7 +39,7 @@ func main() { } r := chi.NewRouter() - registerRoutes(r) + registerRoutes(r, db) port := os.Getenv("PORT") if port == "" { diff --git a/src/templates/go.mod.ejs b/src/templates/go.mod.ejs index 0c83f94..0df3f33 100644 --- a/src/templates/go.mod.ejs +++ b/src/templates/go.mod.ejs @@ -5,4 +5,5 @@ go 1.14 require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-sql-driver/mysql v1.5.0 + github.com/jmoiron/sqlx v1.2.0 ) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index f6d3444..0324d65 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -1,16 +1,19 @@ package main +import "database/sql" import "encoding/json" import "fmt" import "net/http" +import "os" import "strconv" import "github.com/go-chi/chi" +import "github.com/jmoiron/sqlx" type Category struct { - Id int `json:"id"` - Name *string `json:"name"` - NameRu *string `json:"name_ru"` - Slug *string `json:"slug"` + Id int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + NameRu *string `json:"name_ru" db:"name_ru"` + Slug string `json:"slug" db:"slug"` } <% @@ -79,7 +82,7 @@ function addTypes(props) { return { "name": prop, // TODO: resolve/autoguess types - "type": "string" + "type": "*string" } }); } @@ -87,7 +90,7 @@ function addTypes(props) { function query2dto(parser, query) { query = removePlaceholders(query); const queryAst = parser.astify(query); - const props = extractProperties(queryAst).map(snake2camelCase); + const props = extractProperties(queryAst); if (props.length === 0) { console.warn('Could not create DTO for query:', formatQuery(query)); console.debug('Query AST:'); @@ -118,9 +121,11 @@ function snake2camelCase(str) { return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase()); } -// ["a", "bb", "ccc"] => 3 +// ["a", "b__b", "ccc"] => 3 +// Note that it doesn't count underscores. function lengthOfLongestString(arr) { return arr + .map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, '')) .map(el => el.length) .reduce( (acc, val) => val > acc ? val : acc, @@ -132,7 +137,7 @@ function dto2struct(dto) { let result = `type ${dto.name} struct {\n`; dto.props.forEach(prop => { const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength); - result += `\t${fieldName} ${prop.type} \`json:"${prop.name}"\`\n` + result += `\t${fieldName} ${prop.type} \`json:"${prop.name},omitempty" db:"${prop.name}"\`\n` }); result += '}\n'; @@ -166,7 +171,7 @@ endpoints.forEach(function(endpoint) { }); }); -%> -func registerRoutes(r chi.Router) { +func registerRoutes(r chi.Router, db *sqlx.DB) { categories := make(map[int]Category) cnt := 0 <% @@ -177,33 +182,47 @@ endpoints.forEach(function(endpoint) { const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; if (hasGetOne || hasGetMany) { + const dto = query2dto(sqlParser, method.query); + // TODO: do we really need signature and cache? + const cacheKey = dto ? dto.signature : null; + const dataType = hasGetMany ? '[]' + dtoCache[cacheKey] : dtoCache[cacheKey]; + + const params = extractParams(method.query); + const formattedParams = formatParamsAsGolangVararg(params); + const queryFunction = hasGetOne ? 'Get' : 'Select'; + // TODO: handle only particular method (get/post/put) + // TODO: include method/path into an error message %> r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { <% - if (path === '/v1/categories/count') { --%> - w.Header().Set("Content-Type", "application/json; charset=utf-8") - fmt.Fprintf(w, `{"counter": %d}`, len(categories)) -<% - } else if (hasGetMany) { + if (params.length > 0) { -%> - w.Header().Set("Content-Type", "application/json; charset=utf-8") - list := []Category{categories[1]} - json.NewEncoder(w).Encode(&list) -<% - } else { --%> - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - category, exist := categories[id] - if !exist { - w.WriteHeader(http.StatusNotFound) + nstmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - json.NewEncoder(w).Encode(&category) -<% - } -%> + + var result <%- dataType %> + args := map[string]interface{}{ + <%- params.map(p => `"${p.substring(2)}": chi.URLParam(r, "${p.substring(2)}"),`).join('\n\t\t\t') %> + } + err = nstmt.Get(&result, args) +<% } else { -%> + var result <%- dataType %> + err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>") +<% } -%> + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + } }) <% } From 16279aed2dcfc77fb4834168c75173a7d273a7e7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 26 Aug 2020 22:18:34 +0100 Subject: [PATCH 043/346] chore: add support for specifying DTO name with dto.name property For example, for the following mapping: - path: /v1/categories/count get: query: SELECT COUNT(*) AS counter FROM categories dto: name: CounterDto the generated struct will be named "CounterDto" instead of "Dto". Part of #9 --- examples/go/routes.go | 30 ++++++++++++------------------ examples/js/endpoints.yaml | 6 ++++++ src/templates/routes.go.ejs | 30 ++++++++++++------------------ 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 94dd387..9091ebe 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -9,25 +9,18 @@ import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" -type Category struct { - Id int `json:"id" db:"id"` - Name string `json:"name" db:"name"` - NameRu *string `json:"name_ru" db:"name_ru"` - Slug string `json:"slug" db:"slug"` -} - -type Dto1 struct { +type CounterDto struct { Counter *string `json:"counter,omitempty" db:"counter"` } -type Dto3 struct { +type CategoryDto struct { Id *string `json:"id,omitempty" db:"id"` Name *string `json:"name,omitempty" db:"name"` NameRu *string `json:"name_ru,omitempty" db:"name_ru"` Slug *string `json:"slug,omitempty" db:"slug"` } -type Dto4 struct { +type CreateCategoryDto struct { Name *string `json:"name,omitempty" db:"name"` NameRu *string `json:"name_ru,omitempty" db:"name_ru"` Slug *string `json:"slug,omitempty" db:"slug"` @@ -35,11 +28,11 @@ type Dto4 struct { } func registerRoutes(r chi.Router, db *sqlx.DB) { - categories := make(map[int]Category) + categories := make(map[int]CategoryDto) cnt := 0 r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { - var result Dto1 + var result CounterDto err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories") switch err { case sql.ErrNoRows: @@ -61,7 +54,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { return } - var result Dto1 + var result CounterDto args := map[string]interface{}{ "collectionId": chi.URLParam(r, "collectionId"), } @@ -79,7 +72,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - var result []Dto3 + var result []CategoryDto err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories") switch err { case sql.ErrNoRows: @@ -94,10 +87,11 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - var category Category + var category CategoryDto json.NewDecoder(r.Body).Decode(&category) cnt += 1 - category.Id = cnt + id := strconv.Itoa(cnt) + category.Id = &id categories[cnt] = category w.WriteHeader(http.StatusNoContent) }) @@ -110,7 +104,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { return } - var result Dto3 + var result CategoryDto args := map[string]interface{}{ "categoryId": chi.URLParam(r, "categoryId"), } @@ -129,7 +123,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - var category Category + var category CategoryDto json.NewDecoder(r.Body).Decode(&category) categories[id] = category w.WriteHeader(http.StatusNoContent) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 6e4f4c7..6b9b33e 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -1,6 +1,8 @@ - path: /v1/categories/count get: query: SELECT COUNT(*) AS counter FROM categories + dto: + name: CounterDto - path: /v1/collections/:collectionId/categories/count get: @@ -19,6 +21,8 @@ , name_ru , slug FROM categories + dto: + name: CategoryDto post: query: >- INSERT @@ -40,6 +44,8 @@ , NOW() , :b.user_id ) + dto: + name: CreateCategoryDto - path: /v1/categories/:categoryId get: diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 0324d65..6e643f2 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -9,13 +9,6 @@ import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" -type Category struct { - Id int `json:"id" db:"id"` - Name string `json:"name" db:"name"` - NameRu *string `json:"name_ru" db:"name_ru"` - Slug string `json:"slug" db:"slug"` -} - <% // {'columns': // [ @@ -87,8 +80,8 @@ function addTypes(props) { }); } -function query2dto(parser, query) { - query = removePlaceholders(query); +function query2dto(parser, method) { + const query = removePlaceholders(method.query); const queryAst = parser.astify(query); const props = extractProperties(queryAst); if (props.length === 0) { @@ -98,9 +91,10 @@ function query2dto(parser, query) { return null; } const propsWithTypes = addTypes(props); + const hasName = method.dto && method.dto.name && method.dto.name.length > 0; + const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; return { - // TODO: assign DTO name dynamically - "name": "Dto" + ++globalDtoCounter, + "name": name, "props": propsWithTypes, // max length is needed for proper formatting "maxFieldNameLength": lengthOfLongestString(props), @@ -159,8 +153,7 @@ const verbs_with_dto = [ 'get', 'post', 'put' ] endpoints.forEach(function(endpoint) { const dtos = endpoint.methods .filter(method => verbs_with_dto.includes(method.verb)) - .map(method => method.query) - .map(query => query2dto(sqlParser, query)) + .map(method => query2dto(sqlParser, method)) .filter(elem => elem) // filter out nulls .filter(dto => !dtoInCache(dto)) .map(dto => dto2struct(cacheDto(dto))) @@ -172,7 +165,7 @@ endpoints.forEach(function(endpoint) { }); -%> func registerRoutes(r chi.Router, db *sqlx.DB) { - categories := make(map[int]Category) + categories := make(map[int]CategoryDto) cnt := 0 <% endpoints.forEach(function(endpoint) { @@ -182,7 +175,7 @@ endpoints.forEach(function(endpoint) { const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; if (hasGetOne || hasGetMany) { - const dto = query2dto(sqlParser, method.query); + const dto = query2dto(sqlParser, method); // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = hasGetMany ? '[]' + dtoCache[cacheKey] : dtoCache[cacheKey]; @@ -229,10 +222,11 @@ endpoints.forEach(function(endpoint) { if (method.name === 'post') { %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var category Category + var category CategoryDto json.NewDecoder(r.Body).Decode(&category) cnt += 1 - category.Id = cnt + id := strconv.Itoa(cnt) + category.Id = &id categories[cnt] = category w.WriteHeader(http.StatusNoContent) }) @@ -242,7 +236,7 @@ endpoints.forEach(function(endpoint) { %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - var category Category + var category CategoryDto json.NewDecoder(r.Body).Decode(&category) categories[id] = category w.WriteHeader(http.StatusNoContent) From b13625b6ad963fec3f02aa0eb266feaea9a35482 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Aug 2020 20:00:40 +0100 Subject: [PATCH 044/346] chore(golang): dto.name should always be used even if another struct can be used instead Part of #9 --- examples/go/routes.go | 9 ++++++++- examples/js/endpoints.yaml | 2 ++ src/templates/routes.go.ejs | 8 +++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 9091ebe..de0ae0a 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -27,6 +27,13 @@ type CreateCategoryDto struct { UserId *string `json:"user_id,omitempty" db:"user_id"` } +type CategoryInfoDto struct { + Id *string `json:"id,omitempty" db:"id"` + Name *string `json:"name,omitempty" db:"name"` + NameRu *string `json:"name_ru,omitempty" db:"name_ru"` + Slug *string `json:"slug,omitempty" db:"slug"` +} + func registerRoutes(r chi.Router, db *sqlx.DB) { categories := make(map[int]CategoryDto) cnt := 0 @@ -104,7 +111,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { return } - var result CategoryDto + var result CategoryInfoDto args := map[string]interface{}{ "categoryId": chi.URLParam(r, "categoryId"), } diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 6b9b33e..3c0150b 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -56,6 +56,8 @@ , slug FROM categories WHERE id = :p.categoryId + dto: + name: CategoryInfoDto put: query: >- UPDATE categories diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 6e643f2..5aa1f61 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -95,6 +95,7 @@ function query2dto(parser, method) { const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; return { "name": name, + "hasUserProvidedName": hasName, "props": propsWithTypes, // max length is needed for proper formatting "maxFieldNameLength": lengthOfLongestString(props), @@ -146,6 +147,10 @@ function cacheDto(dto) { return dto; } function dtoInCache(dto) { + // always prefer user specified name even when we have a similar DTO in cache + if (dto.hasUserProvidedName) { + return false; + } return dtoCache.hasOwnProperty(dto.signature); } @@ -178,7 +183,8 @@ endpoints.forEach(function(endpoint) { const dto = query2dto(sqlParser, method); // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; - const dataType = hasGetMany ? '[]' + dtoCache[cacheKey] : dtoCache[cacheKey]; + const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const dataType = hasGetMany ? '[]' + dtoName : dtoName; const params = extractParams(method.query); const formattedParams = formatParamsAsGolangVararg(params); From 7f283516d95c749bb801d3cd74563aaba9c71d89 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Aug 2020 20:20:52 +0100 Subject: [PATCH 045/346] chore(golang): user can provide a type of DTO member By default, string type is used. Also, the value is optional (nullable). Example of specifying an integer type: dto: fields: counter: type: integer Part of #9 --- examples/go/routes.go | 2 +- examples/js/endpoints.yaml | 7 +++++++ src/templates/routes.go.ejs | 9 +++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index de0ae0a..b2dbcc0 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -10,7 +10,7 @@ import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" type CounterDto struct { - Counter *string `json:"counter,omitempty" db:"counter"` + Counter *integer `json:"counter,omitempty" db:"counter"` } type CategoryDto struct { diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 3c0150b..a1908ec 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -3,6 +3,9 @@ query: SELECT COUNT(*) AS counter FROM categories dto: name: CounterDto + fields: + counter: + type: integer - path: /v1/collections/:collectionId/categories/count get: @@ -12,6 +15,10 @@ JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :p.collectionId + dto: + fields: + counter: + type: integer - path: /v1/categories get_list: diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 5aa1f61..9a014a7 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -70,12 +70,12 @@ function extractProperties(queryAst) { return []; } -function addTypes(props) { +function addTypes(props, fieldsInfo) { return props.map(prop => { + const hasTypeInfo = fieldsInfo.hasOwnProperty(prop) && fieldsInfo[prop].hasOwnProperty('type'); return { "name": prop, - // TODO: resolve/autoguess types - "type": "*string" + "type": hasTypeInfo ? '*' + fieldsInfo[prop].type : '*string' } }); } @@ -90,7 +90,8 @@ function query2dto(parser, method) { console.debug(queryAst); return null; } - const propsWithTypes = addTypes(props); + const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {}; + const propsWithTypes = addTypes(props, fieldsInfo); const hasName = method.dto && method.dto.name && method.dto.name.length > 0; const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; return { From a202a511e7b7dd3ddfe8dfe964c2278fc92331ca Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 29 Aug 2020 16:34:22 +0100 Subject: [PATCH 046/346] chore(golang): convert "integer" to "int" to unbreak compilation Correction for 7f283516d95c749bb801d3cd74563aaba9c71d89 commit. Part of #9 --- examples/go/routes.go | 2 +- src/templates/routes.go.ejs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index b2dbcc0..c0d4d31 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -10,7 +10,7 @@ import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" type CounterDto struct { - Counter *integer `json:"counter,omitempty" db:"counter"` + Counter *int `json:"counter,omitempty" db:"counter"` } type CategoryDto struct { diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 9a014a7..fea9599 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -70,12 +70,20 @@ function extractProperties(queryAst) { return []; } +function findOutType(fieldsInfo, fieldName) { + const defaultType = '*string'; + const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type'); + if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { + return '*int'; + } + return defaultType; +} + function addTypes(props, fieldsInfo) { return props.map(prop => { - const hasTypeInfo = fieldsInfo.hasOwnProperty(prop) && fieldsInfo[prop].hasOwnProperty('type'); return { "name": prop, - "type": hasTypeInfo ? '*' + fieldsInfo[prop].type : '*string' + "type": findOutType(fieldsInfo, prop), } }); } From cb284d0e09c1fb7c21944b6f7a69147241e57d48 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 13:17:11 +0100 Subject: [PATCH 047/346] refactor: remove unused formatParamsAsGolangVararg() --- src/cli.js | 10 ---------- src/templates/routes.go.ejs | 1 - 2 files changed, 11 deletions(-) diff --git a/src/cli.js b/src/cli.js index ed8cb02..db77dac 100755 --- a/src/cli.js +++ b/src/cli.js @@ -127,16 +127,6 @@ const createEndpoints = async (destDir, lang, config) => { : params; }, - // [ "p.page", "b.num" ] => 'chi.URLParam(r, "page"), chi.URLParam(r, "num")' - // (used only with Golang's go-chi) - // TODO: do we need to de-deduplicate (new Set(params))? - // TODO: handle b.params - "formatParamsAsGolangVararg": (params) => { - return params.length > 0 - ? Array.from(params, p => `chi.URLParam(r, "${p.substring(2)}")`).join(', ') - : params; - }, - // "SELECT *\n FROM foo" => "SELECT * FROM foo" "formatQuery": (query) => { return removePlaceholders(flattenQuery(query)); diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index fea9599..9dd7605 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -196,7 +196,6 @@ endpoints.forEach(function(endpoint) { const dataType = hasGetMany ? '[]' + dtoName : dtoName; const params = extractParams(method.query); - const formattedParams = formatParamsAsGolangVararg(params); const queryFunction = hasGetOne ? 'Get' : 'Select'; // TODO: handle only particular method (get/post/put) // TODO: include method/path into an error message From 38cafe8898e48c0f9b52dd9c0d4f442e56553061 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 14:51:57 +0100 Subject: [PATCH 048/346] refactor: move 2 functions from routes.go.ejs to cli.js --- src/cli.js | 10 ++++++++++ src/templates/routes.go.ejs | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cli.js b/src/cli.js index db77dac..2253c60 100755 --- a/src/cli.js +++ b/src/cli.js @@ -86,6 +86,14 @@ const removePlaceholders = (query) => query.replace(/(?<=:)[pb]\./g, ''); // (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, group1) => group1.toUpperCase()); + +// "nameRu" => "NameRu" +// (used only with Golang's go-chi) +const capitalize = (str) => str[0].toUpperCase() + str.slice(1); + const createEndpoints = async (destDir, lang, config) => { const fileName = `routes.${lang}` console.log('Generate', fileName); @@ -140,6 +148,8 @@ const createEndpoints = async (destDir, lang, config) => { // (used only with Golang) "removePlaceholders": removePlaceholders, + "snake2camelCase": snake2camelCase, + "capitalize": capitalize, } ); diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 9dd7605..4d0edd4 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -115,16 +115,6 @@ function query2dto(parser, method) { }; } -// "nameRu" => "NameRu" -function capitalize(str) { - return str[0].toUpperCase() + str.slice(1); -} - -// "name_ru" => "nameRu" -function snake2camelCase(str) { - return str.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase()); -} - // ["a", "b__b", "ccc"] => 3 // Note that it doesn't count underscores. function lengthOfLongestString(arr) { From 08b02b197e5ad8ef844ad4562b45f102db48229a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 14:54:13 +0100 Subject: [PATCH 049/346] chore(golang): implement logic for POST requests Part of #9 --- examples/go/routes.go | 26 +++++++++++++++++++------- src/cli.js | 13 +++++++++++++ src/templates/routes.go.ejs | 30 +++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index c0d4d31..c7c8093 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -36,7 +36,6 @@ type CategoryInfoDto struct { func registerRoutes(r chi.Router, db *sqlx.DB) { categories := make(map[int]CategoryDto) - cnt := 0 r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { var result CounterDto @@ -94,12 +93,25 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - var category CategoryDto - json.NewDecoder(r.Body).Decode(&category) - cnt += 1 - id := strconv.Itoa(cnt) - category.Id = &id - categories[cnt] = category + var dto CreateCategoryDto + json.NewDecoder(r.Body).Decode(&dto) + + args := map[string]interface{}{ + "name": dto.Name, + "name_ru": dto.NameRu, + "slug": dto.Slug, + "user_id": dto.UserId, + } + _, err := db.NamedExec( + "INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) diff --git a/src/cli.js b/src/cli.js index 2253c60..5584b54 100755 --- a/src/cli.js +++ b/src/cli.js @@ -150,6 +150,19 @@ const createEndpoints = async (destDir, lang, config) => { "removePlaceholders": removePlaceholders, "snake2camelCase": snake2camelCase, "capitalize": capitalize, + + // [ "p.page", "b.num" ] => '"page": dto.Page),\n\t\t\t"num": dto.Num),' + // TODO: add support for non-body params (like path params) + // (used only with Golang's go-chi) + "formatParamsAsGolangMap": (params) => { + if (params.length === 0) { + return params; + } + return Array.from( + new Set(params), + p => `"${p.substring(2)}": dto.${capitalize(snake2camelCase(p.substring(2)))},` + ).join('\n\t\t\t'); + }, } ); diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 4d0edd4..e757fda 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -170,7 +170,6 @@ endpoints.forEach(function(endpoint) { -%> func registerRoutes(r chi.Router, db *sqlx.DB) { categories := make(map[int]CategoryDto) - cnt := 0 <% endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); @@ -224,14 +223,31 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'post') { + const dto = query2dto(sqlParser, method); + // TODO: do we really need signature and cache? + const cacheKey = dto ? dto.signature : null; + const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + + // TODO: align args properly (like gofmt does) + const params = extractParams(method.query); %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var category CategoryDto - json.NewDecoder(r.Body).Decode(&category) - cnt += 1 - id := strconv.Itoa(cnt) - category.Id = &id - categories[cnt] = category + var dto <%- dataType %> + json.NewDecoder(r.Body).Decode(&dto) + + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params) %> + } + _, err := db.NamedExec( + "<%- formatQuery(method.query) %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) <% From 4bf78668cc8d5f510aa4a248bb37e4be06021857 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 14:54:50 +0100 Subject: [PATCH 050/346] style: group functions --- src/cli.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cli.js b/src/cli.js index 5584b54..b5942ff 100755 --- a/src/cli.js +++ b/src/cli.js @@ -140,13 +140,9 @@ const createEndpoints = async (destDir, lang, config) => { return removePlaceholders(flattenQuery(query)); }, - // (used only with Golang's go-chi) - "convertPathPlaceholders": convertPathPlaceholders, - // (used only with Golang) + "convertPathPlaceholders": convertPathPlaceholders, "sqlParser": parser, - - // (used only with Golang) "removePlaceholders": removePlaceholders, "snake2camelCase": snake2camelCase, "capitalize": capitalize, From ff420eea0dcead59ab65ff069138d31c56774d0e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 15:07:46 +0100 Subject: [PATCH 051/346] refactor: introduce language-specific prefixes for accessing parameters --- src/cli.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cli.js b/src/cli.js index b5942ff..756bce5 100755 --- a/src/cli.js +++ b/src/cli.js @@ -113,8 +113,14 @@ const createEndpoints = async (destDir, lang, config) => { } const placeholdersMap = { - 'p': 'req.params', - 'b': 'req.body' + 'js': { + 'p': 'req.params', + 'b': 'req.body', + }, + 'go': { + 'p': 'dto', + 'b': 'dto', + } } const parser = new Parser(); @@ -131,7 +137,7 @@ const createEndpoints = async (destDir, lang, config) => { // (used only with Express) "formatParamsAsJavaScriptObject": (params) => { return params.length > 0 - ? '{ ' + Array.from(new Set(params), p => `"${p.substring(2)}": ${placeholdersMap[p.substring(0, 1)]}.${p.substring(2)}`).join(', ') + ' }' + ? '{ ' + Array.from(new Set(params), p => `"${p.substring(2)}": ${placeholdersMap['js'][p.substring(0, 1)]}.${p.substring(2)}`).join(', ') + ' }' : params; }, @@ -156,7 +162,7 @@ const createEndpoints = async (destDir, lang, config) => { } return Array.from( new Set(params), - p => `"${p.substring(2)}": dto.${capitalize(snake2camelCase(p.substring(2)))},` + p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)]}.${capitalize(snake2camelCase(p.substring(2)))},` ).join('\n\t\t\t'); }, } From 6a02b2b9a5e14143450158971436f0b44a36b045 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 15:18:14 +0100 Subject: [PATCH 052/346] refactor(golang): extract variables --- src/cli.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 756bce5..d7eb42d 100755 --- a/src/cli.js +++ b/src/cli.js @@ -162,7 +162,12 @@ const createEndpoints = async (destDir, lang, config) => { } return Array.from( new Set(params), - p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)]}.${capitalize(snake2camelCase(p.substring(2)))},` + p => { + const bindTarget = p.substring(0, 1); + const paramName = p.substring(2); + const fieldName = capitalize(snake2camelCase(paramName)); + return `"${paramName}": ${placeholdersMap['go'][bindTarget]}.${fieldName},` + } ).join('\n\t\t\t'); }, } From 2870482be746b9da5aa1ecb856bb032577d755df Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 15:39:08 +0100 Subject: [PATCH 053/346] chore(golang): implement logic for PUT requests Part of #9 --- examples/go/routes.go | 24 ++++++++++++++++++++---- src/cli.js | 12 ++++++++---- src/templates/routes.go.ejs | 27 +++++++++++++++++++++++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index c7c8093..bc81f63 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -141,10 +141,26 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - var category CategoryDto - json.NewDecoder(r.Body).Decode(&category) - categories[id] = category + var dto CreateCategoryDto + json.NewDecoder(r.Body).Decode(&dto) + + args := map[string]interface{}{ + "name": dto.Name, + "name_ru": dto.NameRu, + "slug": dto.Slug, + "user_id": dto.UserId, + "categoryId": chi.URLParam(r, "categoryId"), + } + _, err := db.NamedExec( + "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) diff --git a/src/cli.js b/src/cli.js index d7eb42d..3fb59dd 100755 --- a/src/cli.js +++ b/src/cli.js @@ -118,8 +118,12 @@ const createEndpoints = async (destDir, lang, config) => { 'b': 'req.body', }, 'go': { - 'p': 'dto', - 'b': 'dto', + 'p': function(param) { + return `chi.URLParam(r, "${param}")` + }, + 'b': function(param) { + return 'dto.' + capitalize(snake2camelCase(param)); + }, } } @@ -165,8 +169,8 @@ const createEndpoints = async (destDir, lang, config) => { p => { const bindTarget = p.substring(0, 1); const paramName = p.substring(2); - const fieldName = capitalize(snake2camelCase(paramName)); - return `"${paramName}": ${placeholdersMap['go'][bindTarget]}.${fieldName},` + const formatFunc = placeholdersMap['go'][bindTarget]; + return `"${paramName}": ${formatFunc(paramName)},` } ).join('\n\t\t\t'); }, diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index e757fda..e154538 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -253,12 +253,31 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'put') { + const dto = query2dto(sqlParser, method); + // TODO: do we really need signature and cache? + const cacheKey = dto ? dto.signature : null; + const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + + // TODO: align args properly (like gofmt does) + const params = extractParams(method.query); %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - var category CategoryDto - json.NewDecoder(r.Body).Decode(&category) - categories[id] = category + var dto <%- dataType %> + json.NewDecoder(r.Body).Decode(&dto) + + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params) %> + } + _, err := db.NamedExec( + "<%- formatQuery(method.query) %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) <% From e6e1ee31d1ca4ee925d0da323869b6157feba701 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 15:41:59 +0100 Subject: [PATCH 054/346] chore(golang): don't omit null values in order to pass our tests Part of #9 --- examples/go/routes.go | 26 +++++++++++++------------- src/templates/routes.go.ejs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index bc81f63..5f76228 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -10,28 +10,28 @@ import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" type CounterDto struct { - Counter *int `json:"counter,omitempty" db:"counter"` + Counter *int `json:"counter" db:"counter"` } type CategoryDto struct { - Id *string `json:"id,omitempty" db:"id"` - Name *string `json:"name,omitempty" db:"name"` - NameRu *string `json:"name_ru,omitempty" db:"name_ru"` - Slug *string `json:"slug,omitempty" db:"slug"` + Id *string `json:"id" db:"id"` + Name *string `json:"name" db:"name"` + NameRu *string `json:"name_ru" db:"name_ru"` + Slug *string `json:"slug" db:"slug"` } type CreateCategoryDto struct { - Name *string `json:"name,omitempty" db:"name"` - NameRu *string `json:"name_ru,omitempty" db:"name_ru"` - Slug *string `json:"slug,omitempty" db:"slug"` - UserId *string `json:"user_id,omitempty" db:"user_id"` + Name *string `json:"name" db:"name"` + NameRu *string `json:"name_ru" db:"name_ru"` + Slug *string `json:"slug" db:"slug"` + UserId *string `json:"user_id" db:"user_id"` } type CategoryInfoDto struct { - Id *string `json:"id,omitempty" db:"id"` - Name *string `json:"name,omitempty" db:"name"` - NameRu *string `json:"name_ru,omitempty" db:"name_ru"` - Slug *string `json:"slug,omitempty" db:"slug"` + Id *string `json:"id" db:"id"` + Name *string `json:"name" db:"name"` + NameRu *string `json:"name_ru" db:"name_ru"` + Slug *string `json:"slug" db:"slug"` } func registerRoutes(r chi.Router, db *sqlx.DB) { diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index e154538..67fafbe 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -131,7 +131,7 @@ function dto2struct(dto) { let result = `type ${dto.name} struct {\n`; dto.props.forEach(prop => { const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength); - result += `\t${fieldName} ${prop.type} \`json:"${prop.name},omitempty" db:"${prop.name}"\`\n` + result += `\t${fieldName} ${prop.type} \`json:"${prop.name}" db:"${prop.name}"\`\n` }); result += '}\n'; From d85d6f544cb68d2fb71b83caab4a09fbb37ec9ba Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 15:45:26 +0100 Subject: [PATCH 055/346] chore(golang): update endpoints.yaml to make ID field to be an integer This unbreaks some of our tests. Part of #9 --- examples/go/routes.go | 4 ++-- examples/js/endpoints.yaml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index 5f76228..c9ddff3 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -14,7 +14,7 @@ type CounterDto struct { } type CategoryDto struct { - Id *string `json:"id" db:"id"` + Id *int `json:"id" db:"id"` Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` @@ -28,7 +28,7 @@ type CreateCategoryDto struct { } type CategoryInfoDto struct { - Id *string `json:"id" db:"id"` + Id *int `json:"id" db:"id"` Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index a1908ec..ae1e938 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -30,6 +30,9 @@ FROM categories dto: name: CategoryDto + fields: + id: + type: integer post: query: >- INSERT @@ -65,6 +68,9 @@ WHERE id = :p.categoryId dto: name: CategoryInfoDto + fields: + id: + type: integer put: query: >- UPDATE categories From e4e466961b8133372962d97c3d1bc3d5b1ff9f66 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 19:30:33 +0100 Subject: [PATCH 056/346] chore(golang): implement logic for DELETE requests Part of #9 --- examples/go/routes.go | 17 +++++++++++++---- src/templates/routes.go.ejs | 23 ++++++++++++++--------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index c9ddff3..c1d2476 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -5,7 +5,6 @@ import "encoding/json" import "fmt" import "net/http" import "os" -import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" @@ -35,7 +34,6 @@ type CategoryInfoDto struct { } func registerRoutes(r chi.Router, db *sqlx.DB) { - categories := make(map[int]CategoryDto) r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { var result CounterDto @@ -165,8 +163,19 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - delete(categories, id) + args := map[string]interface{}{ + "categoryId": chi.URLParam(r, "categoryId"), + } + _, err := db.NamedExec( + "DELETE FROM categories WHERE id = :categoryId", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 67fafbe..e00975d 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -5,7 +5,6 @@ import "encoding/json" import "fmt" import "net/http" import "os" -import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" @@ -169,12 +168,12 @@ endpoints.forEach(function(endpoint) { }); -%> func registerRoutes(r chi.Router, db *sqlx.DB) { - categories := make(map[int]CategoryDto) <% endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); endpoint.methods.forEach(function(method) { + const params = extractParams(method.query); const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; if (hasGetOne || hasGetMany) { @@ -184,7 +183,6 @@ endpoints.forEach(function(endpoint) { const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; const dataType = hasGetMany ? '[]' + dtoName : dtoName; - const params = extractParams(method.query); const queryFunction = hasGetOne ? 'Get' : 'Select'; // TODO: handle only particular method (get/post/put) // TODO: include method/path into an error message @@ -227,9 +225,7 @@ endpoints.forEach(function(endpoint) { // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - // TODO: align args properly (like gofmt does) - const params = extractParams(method.query); %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var dto <%- dataType %> @@ -257,9 +253,7 @@ endpoints.forEach(function(endpoint) { // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - // TODO: align args properly (like gofmt does) - const params = extractParams(method.query); %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var dto <%- dataType %> @@ -285,8 +279,19 @@ endpoints.forEach(function(endpoint) { if (method.name === 'delete') { %> r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.Atoi(chi.URLParam(r, "categoryId")) - delete(categories, id) + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params) %> + } + _, err := db.NamedExec( + "<%- formatQuery(method.query) %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) }) <% From cae5201e5240a4a13b7013b5bf2cac8b3928d17c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 19:56:10 +0100 Subject: [PATCH 057/346] chore: remove outdaled TODO --- src/cli.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 3fb59dd..b2f5190 100755 --- a/src/cli.js +++ b/src/cli.js @@ -158,7 +158,6 @@ const createEndpoints = async (destDir, lang, config) => { "capitalize": capitalize, // [ "p.page", "b.num" ] => '"page": dto.Page),\n\t\t\t"num": dto.Num),' - // TODO: add support for non-body params (like path params) // (used only with Golang's go-chi) "formatParamsAsGolangMap": (params) => { if (params.length === 0) { From 9e28321efca98a0c724f9b80f58e03ca727b3dce Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 19:59:11 +0100 Subject: [PATCH 058/346] chore(golang): improve code formatting Part of #9 --- examples/go/routes.go | 12 ++++++------ src/cli.js | 16 +++++++++++++++- src/templates/routes.go.ejs | 16 +--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/go/routes.go b/examples/go/routes.go index c1d2476..0db1a33 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -95,9 +95,9 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewDecoder(r.Body).Decode(&dto) args := map[string]interface{}{ - "name": dto.Name, + "name": dto.Name, "name_ru": dto.NameRu, - "slug": dto.Slug, + "slug": dto.Slug, "user_id": dto.UserId, } _, err := db.NamedExec( @@ -143,10 +143,10 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewDecoder(r.Body).Decode(&dto) args := map[string]interface{}{ - "name": dto.Name, - "name_ru": dto.NameRu, - "slug": dto.Slug, - "user_id": dto.UserId, + "name": dto.Name, + "name_ru": dto.NameRu, + "slug": dto.Slug, + "user_id": dto.UserId, "categoryId": chi.URLParam(r, "categoryId"), } _, err := db.NamedExec( diff --git a/src/cli.js b/src/cli.js index b2f5190..4daaad3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -94,6 +94,15 @@ const snake2camelCase = (str) => str.replace(/_([a-z])/g, (match, group1) => gro // (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 fileName = `routes.${lang}` console.log('Generate', fileName); @@ -156,6 +165,7 @@ const createEndpoints = async (destDir, lang, config) => { "removePlaceholders": removePlaceholders, "snake2camelCase": snake2camelCase, "capitalize": capitalize, + "lengthOfLongestString": lengthOfLongestString, // [ "p.page", "b.num" ] => '"page": dto.Page),\n\t\t\t"num": dto.Num),' // (used only with Golang's go-chi) @@ -163,13 +173,17 @@ const createEndpoints = async (destDir, lang, config) => { 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]; - return `"${paramName}": ${formatFunc(paramName)},` + 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'); }, diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index e00975d..2b9dd1d 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -106,7 +106,7 @@ function query2dto(parser, method) { "hasUserProvidedName": hasName, "props": propsWithTypes, // max length is needed for proper formatting - "maxFieldNameLength": lengthOfLongestString(props), + "maxFieldNameLength": lengthOfLongestString(props.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))), // required for de-duplication // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" // TODO: sort before join @@ -114,18 +114,6 @@ function query2dto(parser, method) { }; } -// ["a", "b__b", "ccc"] => 3 -// Note that it doesn't count underscores. -function lengthOfLongestString(arr) { - return arr - .map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, '')) - .map(el => el.length) - .reduce( - (acc, val) => val > acc ? val : acc, - 0 /* initial value */ - ); -} - function dto2struct(dto) { let result = `type ${dto.name} struct {\n`; dto.props.forEach(prop => { @@ -225,7 +213,6 @@ endpoints.forEach(function(endpoint) { // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - // TODO: align args properly (like gofmt does) %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var dto <%- dataType %> @@ -253,7 +240,6 @@ endpoints.forEach(function(endpoint) { // TODO: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - // TODO: align args properly (like gofmt does) %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var dto <%- dataType %> From e7cd349f35bad03a3926eca6fc2caa2fede307f6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 20:12:41 +0100 Subject: [PATCH 059/346] refactor: improve formatParamsAsJavaScriptObject() redability --- src/cli.js | 17 +++++++++++++---- src/templates/routes.js.ejs | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/cli.js b/src/cli.js index 4daaad3..345e25c 100755 --- a/src/cli.js +++ b/src/cli.js @@ -146,12 +146,21 @@ const createEndpoints = async (destDir, lang, config) => { // "... WHERE id = :p.id" => [ "p.id" ] => [ "p.id" ] "extractParams": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], - // [ "p.page", "b.num" ] => '{ "page" : req.params.page, "num": req.body.num }' + // [ "p.page", "b.num" ] => '"page": req.params.page, "num": req.body.num' // (used only with Express) "formatParamsAsJavaScriptObject": (params) => { - return params.length > 0 - ? '{ ' + Array.from(new Set(params), p => `"${p.substring(2)}": ${placeholdersMap['js'][p.substring(0, 1)]}.${p.substring(2)}`).join(', ') + ' }' - : 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" => "SELECT * FROM foo" diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 8155cda..f5b2925 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -10,7 +10,7 @@ endpoints.forEach(function(endpoint) { const sql = formatQuery(method.query); const params = extractParams(method.query); const formattedParams = params.length > 0 - ? '\n ' + formatParamsAsJavaScriptObject(params) + ',' + ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' : '' if (hasGetOne || hasGetMany) { From 9b43f71be0155f057fa5dd99231d99204ab34881 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 30 Aug 2020 20:24:32 +0100 Subject: [PATCH 060/346] style: correct a comment --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 345e25c..03d1fa2 100755 --- a/src/cli.js +++ b/src/cli.js @@ -176,7 +176,7 @@ const createEndpoints = async (destDir, lang, config) => { "capitalize": capitalize, "lengthOfLongestString": lengthOfLongestString, - // [ "p.page", "b.num" ] => '"page": dto.Page),\n\t\t\t"num": dto.Num),' + // [ "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) { From 87e189080b8d52b8230d08c9d2c62fde364c1ecd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 12 Sep 2022 11:46:42 +0700 Subject: [PATCH 061/346] docs: use tables for formatting and add a list of used libraries --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 32004e1..e9db07b 100644 --- a/README.md +++ b/README.md @@ -43,18 +43,16 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) ``` Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. -1. Generate code - ```console - $ npx query2app - ``` - An example of generated code can be inspect at [examples/js](examples/js) directory. +1. Generate code + | Language | Command for code generation | Example of generated files | Libraries | + | -----------| ----------------------------| ---------------------------| --------- | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | 1. Run the application - ```console - $ npm install - $ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password - $ npm start - ``` + | Language | Commands to run the application | + | -----------| --------------------------------| + | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| + --- :bulb: **NOTE** From 8461f96b9ad484b3b7d7941cd859eef915eaa848 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 12 Sep 2022 11:48:39 +0700 Subject: [PATCH 062/346] docs(golang): add instructions for generating/running Golang app Part of #9 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e9db07b..0e1e534 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,13 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | 1. Run the application | Language | Commands to run the application | | -----------| --------------------------------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| + | Golang |
$ go run *.go
or
$ go build -o app
$ ./app
| --- :bulb: **NOTE** From 0d931a0e3259313cb4de1830a36a7f70b9b3fab7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 14 Sep 2022 09:39:50 +0700 Subject: [PATCH 063/346] refactor: introduce a method for converting language name to a file extension --- src/cli.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/cli.js b/src/cli.js index 03d1fa2..0e6e374 100755 --- a/src/cli.js +++ b/src/cli.js @@ -68,12 +68,23 @@ const loadConfig = (endpointsFile) => { } }; +const lang2extension = (lang) => { + switch (lang) { + case 'js': + case 'go': + return lang + default: + throw new Error(`Unsupported language: ${lang}`) + } +} + const createApp = async (destDir, lang) => { - const fileName = `app.${lang}` + const ext = lang2extension(lang) + const fileName = `app.${ext}` console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); - fs.copyFileSync(`${__dirname}/templates/app.${lang}`, resultFile) + fs.copyFileSync(`${__dirname}/templates/app.${ext}`, resultFile) }; // "SELECT *\n FROM foo" => "SELECT * FROM foo" @@ -104,7 +115,8 @@ const lengthOfLongestString = (arr) => arr ); const createEndpoints = async (destDir, lang, config) => { - const fileName = `routes.${lang}` + const ext = lang2extension(lang) + const fileName = `routes.${ext}` console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); @@ -139,7 +151,7 @@ const createEndpoints = async (destDir, lang, config) => { const parser = new Parser(); const resultedCode = await ejs.renderFile( - `${__dirname}/templates/routes.${lang}.ejs`, + `${__dirname}/templates/routes.${ext}.ejs`, { "endpoints": config, From 96678e02a1cf67aeddafb590e7060ed8215f7b15 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 13 Sep 2022 21:13:57 +0700 Subject: [PATCH 064/346] chore(python): add --lang python option and make it possible to generate "Hello world" app Part of #16 --- README.md | 2 ++ examples/python/app.py | 7 +++++++ examples/python/endpoints.yaml | 1 + examples/python/requirements.txt | 2 ++ examples/python/routes.py | 0 src/cli.js | 11 +++++++++++ src/templates/app.py | 7 +++++++ src/templates/requirements.txt.ejs | 2 ++ src/templates/routes.py.ejs | 0 9 files changed, 32 insertions(+) create mode 100644 examples/python/app.py create mode 120000 examples/python/endpoints.yaml create mode 100644 examples/python/requirements.txt create mode 100644 examples/python/routes.py create mode 100644 src/templates/app.py create mode 100644 src/templates/requirements.txt.ejs create mode 100644 src/templates/routes.py.ejs diff --git a/README.md b/README.md index 0e1e534..c7964b3 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,14 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py), [`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | 1. Run the application | Language | Commands to run the application | | -----------| --------------------------------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | Golang |
$ go run *.go
or
$ go build -o app
$ ./app
| + | Python |
$ pip install -r requirements.txt
$ uvicorn app:app
| --- :bulb: **NOTE** diff --git a/examples/python/app.py b/examples/python/app.py new file mode 100644 index 0000000..221c953 --- /dev/null +++ b/examples/python/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get('/') +def home(): + return { "hello": "world" } diff --git a/examples/python/endpoints.yaml b/examples/python/endpoints.yaml new file mode 120000 index 0000000..ff2e3db --- /dev/null +++ b/examples/python/endpoints.yaml @@ -0,0 +1 @@ +../js/endpoints.yaml \ No newline at end of file diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt new file mode 100644 index 0000000..3fc9c40 --- /dev/null +++ b/examples/python/requirements.txt @@ -0,0 +1,2 @@ +fastapi===0.83.0 +uvicorn==0.18.3 diff --git a/examples/python/routes.py b/examples/python/routes.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cli.js b/src/cli.js index 0e6e374..a77c075 100755 --- a/src/cli.js +++ b/src/cli.js @@ -73,6 +73,8 @@ const lang2extension = (lang) => { case 'js': case 'go': return lang + case 'python': + return 'py' default: throw new Error(`Unsupported language: ${lang}`) } @@ -222,6 +224,9 @@ const createDependenciesDescriptor = async (destDir, lang) => { } else if (lang === 'go') { fileName = 'go.mod' + } else if (lang === 'python') { + fileName = 'requirements.txt' + } else { return; } @@ -259,6 +264,12 @@ 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`) } }; diff --git a/src/templates/app.py b/src/templates/app.py new file mode 100644 index 0000000..221c953 --- /dev/null +++ b/src/templates/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get('/') +def home(): + return { "hello": "world" } diff --git a/src/templates/requirements.txt.ejs b/src/templates/requirements.txt.ejs new file mode 100644 index 0000000..3fc9c40 --- /dev/null +++ b/src/templates/requirements.txt.ejs @@ -0,0 +1,2 @@ +fastapi===0.83.0 +uvicorn==0.18.3 diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs new file mode 100644 index 0000000..e69de29 From d496553f2646f1f8f9fabb24a5ab13574ea2e86f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 14 Sep 2022 10:33:17 +0700 Subject: [PATCH 065/346] chore(python): move routes to routes.py file See https://fastapi.tiangolo.com/tutorial/bigger-applications/ Part of #16 --- README.md | 2 +- examples/python/app.py | 5 ++--- examples/python/routes.py | 7 +++++++ src/templates/app.py | 5 ++--- src/templates/routes.py.ejs | 7 +++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c7964b3..aadbc0b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py), [`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py), [`routes.py`](examples/python/routes.py), [`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | 1. Run the application | Language | Commands to run the application | diff --git a/examples/python/app.py b/examples/python/app.py index 221c953..0043406 100644 --- a/examples/python/app.py +++ b/examples/python/app.py @@ -1,7 +1,6 @@ from fastapi import FastAPI +from routes import router app = FastAPI() -@app.get('/') -def home(): - return { "hello": "world" } +app.include_router(router) diff --git a/examples/python/routes.py b/examples/python/routes.py index e69de29..0b9682b 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get('/') +def home(): + return { "hello": "world" } diff --git a/src/templates/app.py b/src/templates/app.py index 221c953..0043406 100644 --- a/src/templates/app.py +++ b/src/templates/app.py @@ -1,7 +1,6 @@ from fastapi import FastAPI +from routes import router app = FastAPI() -@app.get('/') -def home(): - return { "hello": "world" } +app.include_router(router) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index e69de29..0b9682b 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get('/') +def home(): + return { "hello": "world" } From d2e1dfb4eb499f6d394d3e6762386c9a58aba421 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 14 Sep 2022 10:35:02 +0700 Subject: [PATCH 066/346] style: list Python generated files on separate lines --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aadbc0b..e47d0a1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py), [`routes.py`](examples/python/routes.py), [`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | 1. Run the application | Language | Commands to run the application | From c6aa282c0c920c0a0bbe6e7c11ba4a1f58df2615 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 15 Sep 2022 10:25:26 +0700 Subject: [PATCH 067/346] chore(python): generate stubs for routes Part of #16 --- examples/python/routes.py | 32 ++++++++++++++++++++--- src/cli.js | 7 +++++ src/templates/routes.py.ejs | 51 ++++++++++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 0b9682b..c41e8d7 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -2,6 +2,32 @@ router = APIRouter() -@router.get('/') -def home(): - return { "hello": "world" } + +@router.get('/v1/categories/count') +def get_v1_categories_count(): + pass + +@router.get('/v1/collections/:collectionId/categories/count') +def get_v1_collections_collection_id_categories_count(): + pass + +@router.get('/v1/categories') +def get_list_v1_categories(): + pass + +@router.post('/v1/categories') +def post_v1_categories(): + pass + +@router.get('/v1/categories/:categoryId') +def get_v1_categories_category_id(): + pass + +@router.put('/v1/categories/:categoryId') +def put_v1_categories_category_id(): + pass + +@router.delete('/v1/categories/:categoryId') +def delete_v1_categories_category_id(): + pass + diff --git a/src/cli.js b/src/cli.js index a77c075..c0310d8 100755 --- a/src/cli.js +++ b/src/cli.js @@ -103,6 +103,10 @@ const convertPathPlaceholders = (path) => path.replace(/:([^\/]+)/g, '{$1}'); // (used only with Golang's go-chi) const snake2camelCase = (str) => str.replace(/_([a-z])/g, (match, group1) => group1.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); @@ -190,6 +194,9 @@ const createEndpoints = async (destDir, lang, config) => { "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) => { diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 0b9682b..a202f79 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -2,6 +2,51 @@ from fastapi import APIRouter router = APIRouter() -@router.get('/') -def home(): - return { "hello": "world" } +<% +// { "get", "/v1/categories/:categoryId" } => "get_v1_categories_category_id" +function generate_method_name(method, path) { + const name = camel2snakeCase(path).replace(/\//g, '_').replace(/[^_a-z0-9]/g, ''); + return `${method}${name}` +} + +endpoints.forEach(function(endpoint) { + const path = endpoint.path + + endpoint.methods.forEach(function(method) { + const pythonMethodName = generate_method_name(method.name, path) + + if (method.name === 'get' || method.name === 'get_list') { +%> +@router.get('<%- path %>') +def <%- pythonMethodName %>(): + pass +<% + + } + if (method.name === 'post') { +%> +@router.post('<%- path %>') +def <%- pythonMethodName %>(): + pass +<% + + } + if (method.name === 'put') { +%> +@router.put('<%- path %>') +def <%- pythonMethodName %>(): + pass +<% + + } + if (method.name === 'delete') { +%> +@router.delete('<%- path %>') +def <%- pythonMethodName %>(): + pass +<% + + } + }) +}) +%> From 7808245e7136ea39c5e13572aa51e31eb410f5d2 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 15 Sep 2022 10:25:57 +0700 Subject: [PATCH 068/346] refactor: rename a parameter name --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index c0310d8..67f1591 100755 --- a/src/cli.js +++ b/src/cli.js @@ -101,7 +101,7 @@ 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, group1) => group1.toUpperCase()); +const snake2camelCase = (str) => str.replace(/_([a-z])/g, (match, group) => group.toUpperCase()); // "categoryId" => "category_id" // (used only with Python's FastAPI) From 2a5663e919d77e89c50a696b1afd0557b701410c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 16 Sep 2022 10:21:03 +0700 Subject: [PATCH 069/346] refactor: add suffix to SQL file with scheme --- docker/{categories.sql => categories.mysql.sql} | 0 docker/docker-compose.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docker/{categories.sql => categories.mysql.sql} (100%) diff --git a/docker/categories.sql b/docker/categories.mysql.sql similarity index 100% rename from docker/categories.sql rename to docker/categories.mysql.sql diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ed4b0c8..9c61da9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -17,5 +17,5 @@ services: ports: - '3306:3306' volumes: - - ./categories.sql:/docker-entrypoint-initdb.d/categories.sql + - ./categories.mysql.sql:/docker-entrypoint-initdb.d/categories.sql From 4a9a1912bdbeba37fe05f9b9db9a434b509bb43c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 16 Sep 2022 11:00:02 +0700 Subject: [PATCH 070/346] chore: add scheme for PostgreSQL Part of #15 --- docker/categories.postgres.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docker/categories.postgres.sql diff --git a/docker/categories.postgres.sql b/docker/categories.postgres.sql new file mode 100644 index 0000000..e575563 --- /dev/null +++ b/docker/categories.postgres.sql @@ -0,0 +1,14 @@ +CREATE TABLE categories ( + id bigserial NOT NULL, + name varchar(50) NOT NULL, + name_ru varchar(50) DEFAULT NULL, + slug varchar(50) NOT NULL, + created_at timestamp NOT NULL, + created_by bigint NOT NULL, + updated_at timestamp NOT NULL, + updated_by bigint NOT NULL, + PRIMARY KEY (id), + UNIQUE (name), + UNIQUE (slug), + UNIQUE (name_ru) +); From 6be61e577d35456902956597db247db25e4a692d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 16 Sep 2022 11:00:55 +0700 Subject: [PATCH 071/346] chore(python): implement logic for accessing PostgreSQL and make a simplest GET query work Part of #16 Relate to #15 --- README.md | 4 +-- examples/python/requirements.txt | 1 + examples/python/routes.py | 45 ++++++++++++++++++++++++++++-- src/templates/requirements.txt.ejs | 1 + src/templates/routes.py.ejs | 23 ++++++++++++++- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e47d0a1..a6e2911 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,14 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [](https://pypi.org/project/psycopg2/) | 1. Run the application | Language | Commands to run the application | | -----------| --------------------------------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | Golang |
$ go run *.go
or
$ go build -o app
$ ./app
| - | Python |
$ pip install -r requirements.txt
$ uvicorn app:app
| + | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app
| --- :bulb: **NOTE** diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt index 3fc9c40..680c4cb 100644 --- a/examples/python/requirements.txt +++ b/examples/python/requirements.txt @@ -1,2 +1,3 @@ fastapi===0.83.0 uvicorn==0.18.3 +psycopg2-binary==2.9.3 diff --git a/examples/python/routes.py b/examples/python/routes.py index c41e8d7..5dce104 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -1,3 +1,6 @@ +import os +import psycopg2 + from fastapi import APIRouter router = APIRouter() @@ -5,11 +8,35 @@ @router.get('/v1/categories/count') def get_v1_categories_count(): - pass + conn = psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + try: + with conn: + with conn.cursor() as cur: + cur.execute('SELECT COUNT(*) AS counter FROM categories') + return cur.fetchone()[0] + finally: + conn.close() @router.get('/v1/collections/:collectionId/categories/count') def get_v1_collections_collection_id_categories_count(): - pass + conn = psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + try: + with conn: + with conn.cursor() as cur: + cur.execute('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') + return cur.fetchone()[0] + finally: + conn.close() @router.get('/v1/categories') def get_list_v1_categories(): @@ -21,7 +48,19 @@ def post_v1_categories(): @router.get('/v1/categories/:categoryId') def get_v1_categories_category_id(): - pass + conn = psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + try: + with conn: + with conn.cursor() as cur: + cur.execute('SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId') + return cur.fetchone()[0] + finally: + conn.close() @router.put('/v1/categories/:categoryId') def put_v1_categories_category_id(): diff --git a/src/templates/requirements.txt.ejs b/src/templates/requirements.txt.ejs index 3fc9c40..680c4cb 100644 --- a/src/templates/requirements.txt.ejs +++ b/src/templates/requirements.txt.ejs @@ -1,2 +1,3 @@ fastapi===0.83.0 uvicorn==0.18.3 +psycopg2-binary==2.9.3 diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index a202f79..4515fc9 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -1,3 +1,6 @@ +import os +import psycopg2 + from fastapi import APIRouter router = APIRouter() @@ -14,14 +17,32 @@ endpoints.forEach(function(endpoint) { endpoint.methods.forEach(function(method) { const pythonMethodName = generate_method_name(method.name, path) + const sql = formatQuery(method.query) if (method.name === 'get' || method.name === 'get_list') { %> @router.get('<%- path %>') def <%- pythonMethodName %>(): +<% if (method.name === 'get') { -%> + conn = psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + try: + with conn: + with conn.cursor() as cur: + cur.execute('<%- sql %>') + return cur.fetchone()[0] + finally: + conn.close() +<% + } else { +-%> pass <% - + } } if (method.name === 'post') { %> From 9552e65b64c08f9b5dac76324ab51c233004d581 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 16 Sep 2022 11:03:45 +0700 Subject: [PATCH 072/346] refactor: extract a local variable --- src/templates/routes.py.ejs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 4515fc9..24de948 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -16,14 +16,16 @@ endpoints.forEach(function(endpoint) { const path = endpoint.path endpoint.methods.forEach(function(method) { + const hasGetOne = method.name === 'get' + const hasGetMany = method.name === 'get_list' const pythonMethodName = generate_method_name(method.name, path) const sql = formatQuery(method.query) - if (method.name === 'get' || method.name === 'get_list') { + if (hasGetOne || hasGetMany) { %> @router.get('<%- path %>') def <%- pythonMethodName %>(): -<% if (method.name === 'get') { -%> +<% if (hasGetOne) { -%> conn = psycopg2.connect( database = os.getenv('DB_NAME'), user = os.getenv('DB_USER'), From d8e70cf602f548e32deee887c36ce4d93112f2dd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 16 Sep 2022 11:05:53 +0700 Subject: [PATCH 073/346] chore: fix a link to psycopg2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6e2911..89724dc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [](https://pypi.org/project/psycopg2/) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application | Language | Commands to run the application | From 2e588ff650872679f14735877a14542dbe5a9dc4 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Sep 2022 10:10:59 +0700 Subject: [PATCH 074/346] chore(python): fix generated path parameters Part of #16 --- examples/python/routes.py | 8 ++++---- src/templates/routes.py.ejs | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 5dce104..8546a0e 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -22,7 +22,7 @@ def get_v1_categories_count(): finally: conn.close() -@router.get('/v1/collections/:collectionId/categories/count') +@router.get('/v1/collections/{collectionId}/categories/count') def get_v1_collections_collection_id_categories_count(): conn = psycopg2.connect( database = os.getenv('DB_NAME'), @@ -46,7 +46,7 @@ def get_list_v1_categories(): def post_v1_categories(): pass -@router.get('/v1/categories/:categoryId') +@router.get('/v1/categories/{categoryId}') def get_v1_categories_category_id(): conn = psycopg2.connect( database = os.getenv('DB_NAME'), @@ -62,11 +62,11 @@ def get_v1_categories_category_id(): finally: conn.close() -@router.put('/v1/categories/:categoryId') +@router.put('/v1/categories/{categoryId}') def put_v1_categories_category_id(): pass -@router.delete('/v1/categories/:categoryId') +@router.delete('/v1/categories/{categoryId}') def delete_v1_categories_category_id(): pass diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 24de948..9c9f628 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -12,8 +12,14 @@ function generate_method_name(method, path) { return `${method}${name}` } +// "/categories/:categoryId" => "/categories/{categoryId}" +function convertToFastApiPath(path) { + return path.replace(/:([_a-zA-Z]+)/g, '{$1}') +} + + endpoints.forEach(function(endpoint) { - const path = endpoint.path + const path = convertToFastApiPath(endpoint.path) endpoint.methods.forEach(function(method) { const hasGetOne = method.name === 'get' From 08cb8203ff7bb6bac4448294e726d2803f25f302 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Sep 2022 10:12:13 +0700 Subject: [PATCH 075/346] chore: improve comments --- src/cli.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index 67f1591..3df423f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -161,7 +161,7 @@ const createEndpoints = async (destDir, lang, config) => { { "endpoints": config, - // "... WHERE id = :p.id" => [ "p.id" ] => [ "p.id" ] + // "... WHERE id = :p.id" => [ "p.id" ] "extractParams": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], // [ "p.page", "b.num" ] => '"page": req.params.page, "num": req.body.num' @@ -181,7 +181,7 @@ const createEndpoints = async (destDir, lang, config) => { ).join(', '); }, - // "SELECT *\n FROM foo" => "SELECT * FROM foo" + // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" "formatQuery": (query) => { return removePlaceholders(flattenQuery(query)); }, From 06f48b0364e791902f34e7b4ee850c302e111310 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Sep 2022 10:46:25 +0700 Subject: [PATCH 076/346] refactor: rename method --- src/cli.js | 2 +- src/templates/routes.go.ejs | 2 +- src/templates/routes.js.ejs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.js b/src/cli.js index 3df423f..92476f4 100755 --- a/src/cli.js +++ b/src/cli.js @@ -162,7 +162,7 @@ const createEndpoints = async (destDir, lang, config) => { "endpoints": config, // "... WHERE id = :p.id" => [ "p.id" ] - "extractParams": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], + "extractParamsFromQuery": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], // [ "p.page", "b.num" ] => '"page": req.params.page, "num": req.body.num' // (used only with Express) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 2b9dd1d..12a5fe6 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -161,7 +161,7 @@ endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); endpoint.methods.forEach(function(method) { - const params = extractParams(method.query); + const params = extractParamsFromQuery(method.query); const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; if (hasGetOne || hasGetMany) { diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index f5b2925..d905f54 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -8,7 +8,7 @@ endpoints.forEach(function(endpoint) { const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; const sql = formatQuery(method.query); - const params = extractParams(method.query); + const params = extractParamsFromQuery(method.query); const formattedParams = params.length > 0 ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' : '' From bda5ed8fc6098d8cce86cc74c51352cd67d3440c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Sep 2022 10:56:09 +0700 Subject: [PATCH 077/346] chore(python): implement get request with path parameters Part of #16 --- examples/python/routes.py | 32 +++++++++++++++++++++----------- src/cli.js | 4 ++++ src/templates/routes.py.ejs | 28 ++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 8546a0e..59af942 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -1,7 +1,8 @@ import os import psycopg2 +import psycopg2.extras -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException router = APIRouter() @@ -16,14 +17,17 @@ def get_v1_categories_count(): port = 5432) try: with conn: - with conn.cursor() as cur: + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: cur.execute('SELECT COUNT(*) AS counter FROM categories') - return cur.fetchone()[0] + result = cur.fetchone() + if result is None: + raise HTTPException(status_code=404) + return result finally: conn.close() @router.get('/v1/collections/{collectionId}/categories/count') -def get_v1_collections_collection_id_categories_count(): +def get_v1_collections_collection_id_categories_count(collectionId): conn = psycopg2.connect( database = os.getenv('DB_NAME'), user = os.getenv('DB_USER'), @@ -32,9 +36,12 @@ def get_v1_collections_collection_id_categories_count(): port = 5432) try: with conn: - with conn.cursor() as cur: - cur.execute('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') - return cur.fetchone()[0] + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + cur.execute('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)s', { "collectionId": collectionId }) + result = cur.fetchone() + if result is None: + raise HTTPException(status_code=404) + return result finally: conn.close() @@ -47,7 +54,7 @@ def post_v1_categories(): pass @router.get('/v1/categories/{categoryId}') -def get_v1_categories_category_id(): +def get_v1_categories_category_id(categoryId): conn = psycopg2.connect( database = os.getenv('DB_NAME'), user = os.getenv('DB_USER'), @@ -56,9 +63,12 @@ def get_v1_categories_category_id(): port = 5432) try: with conn: - with conn.cursor() as cur: - cur.execute('SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId') - return cur.fetchone()[0] + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + cur.execute('SELECT id , name , name_ru , slug FROM categories WHERE id = %(categoryId)s', { "categoryId": categoryId }) + result = cur.fetchone() + if result is None: + raise HTTPException(status_code=404) + return result finally: conn.close() diff --git a/src/cli.js b/src/cli.js index 92476f4..ecbeeb4 100755 --- a/src/cli.js +++ b/src/cli.js @@ -164,6 +164,10 @@ const createEndpoints = async (destDir, lang, 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) => { diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 9c9f628..b966a87 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -1,7 +1,8 @@ import os import psycopg2 +import psycopg2.extras -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException router = APIRouter() @@ -12,6 +13,12 @@ function generate_method_name(method, path) { return `${method}${name}` } +// "INSERT INTO ... VALUES(:categoryId)" => "INSERT INTO ... VALUES(%(categoryId)s)" +// See: https://www.psycopg.org/docs/usage.html#passing-parameters-to-sql-queries +function convertToPsycopgNamedArguments(sql) { + return sql.replace(/:([_a-zA-Z]+)/g, '%($1)s') +} + // "/categories/:categoryId" => "/categories/{categoryId}" function convertToFastApiPath(path) { return path.replace(/:([_a-zA-Z]+)/g, '{$1}') @@ -20,17 +27,23 @@ function convertToFastApiPath(path) { endpoints.forEach(function(endpoint) { const path = convertToFastApiPath(endpoint.path) + const paramsFromPath = extractParamsFromPath(endpoint.path) endpoint.methods.forEach(function(method) { const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' const pythonMethodName = generate_method_name(method.name, path) - const sql = formatQuery(method.query) + const sql = convertToPsycopgNamedArguments(formatQuery(method.query)) + const params = extractParamsFromQuery(method.query); + const formattedParams = params.length > 0 + // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] + ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' + : '' if (hasGetOne || hasGetMany) { %> @router.get('<%- path %>') -def <%- pythonMethodName %>(): +def <%- pythonMethodName %>(<%- paramsFromPath.join(', ') %>): <% if (hasGetOne) { -%> conn = psycopg2.connect( database = os.getenv('DB_NAME'), @@ -40,9 +53,12 @@ def <%- pythonMethodName %>(): port = 5432) try: with conn: - with conn.cursor() as cur: - cur.execute('<%- sql %>') - return cur.fetchone()[0] + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + cur.execute('<%- sql %>'<%- formattedParams %>) + result = cur.fetchone() + if result is None: + raise HTTPException(status_code=404) + return result finally: conn.close() <% From a1b7756d1811a6c8e65c5f6bf9fb93caaffd4eb5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 28 Sep 2022 10:15:31 +0700 Subject: [PATCH 078/346] chore(python): specify required Python version in requirements.txt Part of #16 --- examples/python/requirements.txt | 2 +- src/templates/requirements.txt.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt index 680c4cb..271ccad 100644 --- a/examples/python/requirements.txt +++ b/examples/python/requirements.txt @@ -1,3 +1,3 @@ -fastapi===0.83.0 +fastapi===0.83.0; python_version >= "3.6" uvicorn==0.18.3 psycopg2-binary==2.9.3 diff --git a/src/templates/requirements.txt.ejs b/src/templates/requirements.txt.ejs index 680c4cb..271ccad 100644 --- a/src/templates/requirements.txt.ejs +++ b/src/templates/requirements.txt.ejs @@ -1,3 +1,3 @@ -fastapi===0.83.0 +fastapi===0.83.0; python_version >= "3.6" uvicorn==0.18.3 psycopg2-binary==2.9.3 From beb9e8b146c5a19fe66ccd1aa1502288c55692e7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 28 Sep 2022 10:37:33 +0700 Subject: [PATCH 079/346] chore: modify an output to show SQL query on a new line Part of #17 --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index ecbeeb4..ff4aeca 100755 --- a/src/cli.js +++ b/src/cli.js @@ -135,7 +135,7 @@ const createEndpoints = async (destDir, lang, config) => { const sql = removePlaceholders(flattenQuery(method.query)); const verb = method.verb.toUpperCase(); - console.log(`${verb} ${path} => ${sql}`); + console.log(`${verb} ${path}\n\t${sql}`); }); } From 159663a0f1594e81e2533cb2902655a1e8b85699 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 28 Sep 2022 10:42:17 +0700 Subject: [PATCH 080/346] refactor: treat a sinqle query as a list with one element Part of #17 --- src/cli.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index ff4aeca..ddd85cc 100755 --- a/src/cli.js +++ b/src/cli.js @@ -132,10 +132,17 @@ const createEndpoints = async (destDir, lang, config) => { path = convertPathPlaceholders(path) } endpoint.methods.forEach(method => { - const sql = removePlaceholders(flattenQuery(method.query)); const verb = method.verb.toUpperCase(); + console.log(`${verb} ${path}`); - console.log(`${verb} ${path}\n\t${sql}`); + let queries = [] + if (method.query) { + queries.push(method.query) + } + queries.forEach(query => { + const sql = removePlaceholders(flattenQuery(query)); + console.log(`\t${sql}`); + }) }); } From 10a4915e14977a04ed70b4f5f50db4c9c6bcfc88 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 28 Sep 2022 11:09:57 +0700 Subject: [PATCH 081/346] refactor: treat a sinqle query as a list with one element Part of #17 --- src/templates/routes.py.ejs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index b966a87..5d9b8ec 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -33,12 +33,21 @@ endpoints.forEach(function(endpoint) { const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' const pythonMethodName = generate_method_name(method.name, path) - const sql = convertToPsycopgNamedArguments(formatQuery(method.query)) - const params = extractParamsFromQuery(method.query); - const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] - ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' - : '' + + let queries = [] + if (method.query) { + queries.push(method.query) + } + + queries = queries.map(query => { + const sql = convertToPsycopgNamedArguments(formatQuery(query)) + const params = extractParamsFromQuery(query); + const formattedParams = params.length > 0 + // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] + ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' + : '' + return ({ sql : sql, formattedParams: formattedParams }) + }) if (hasGetOne || hasGetMany) { %> @@ -54,11 +63,13 @@ def <%- pythonMethodName %>(<%- paramsFromPath.join(', ') %>): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute('<%- sql %>'<%- formattedParams %>) +<% queries.forEach(query => { -%> + cur.execute('<%- query.sql %>'<%- query.formattedParams %>) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) return result +<% }) -%> finally: conn.close() <% From 75e20918a6587bd888359da946aaa8332f45db51 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 29 Sep 2022 10:50:14 +0700 Subject: [PATCH 082/346] feat(python): add support to aggregate multiple queries within a single response Part of #17 --- examples/js/endpoints.yaml | 8 +++++++ examples/python/routes.py | 24 +++++++++++++++++++ src/cli.js | 2 ++ src/templates/routes.py.ejs | 46 +++++++++++++++++++++++++++---------- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index ae1e938..7d1d09a 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -7,6 +7,14 @@ counter: type: integer +- path: /v1/categories/stat + get: + aggregated_queries: + total: SELECT COUNT(*) FROM categories + in_russian: SELECT COUNT(*) FROM categories WHERE name_ru IS NOT NULL + in_english: SELECT COUNT(*) FROM categories WHERE name IS NOT NULL + fully_translated: SELECT COUNT(*) FROM categories WHERE name IS NOT NULL AND name_ru IS NOT NULL + - path: /v1/collections/:collectionId/categories/count get: query: >- diff --git a/examples/python/routes.py b/examples/python/routes.py index 59af942..2b31089 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -26,6 +26,30 @@ def get_v1_categories_count(): finally: conn.close() +@router.get('/v1/categories/stat') +def get_v1_categories_stat(): + conn = psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + try: + with conn: + with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: + result = {} + cur.execute('SELECT COUNT(*) FROM categories') + result['total'] = cur.fetchone()[0] + cur.execute('SELECT COUNT(*) FROM categories WHERE name_ru IS NOT NULL') + result['in_russian'] = cur.fetchone()[0] + cur.execute('SELECT COUNT(*) FROM categories WHERE name IS NOT NULL') + result['in_english'] = cur.fetchone()[0] + cur.execute('SELECT COUNT(*) FROM categories WHERE name IS NOT NULL AND name_ru IS NOT NULL') + result['fully_translated'] = cur.fetchone()[0] + return result + finally: + conn.close() + @router.get('/v1/collections/{collectionId}/categories/count') def get_v1_collections_collection_id_categories_count(collectionId): conn = psycopg2.connect( diff --git a/src/cli.js b/src/cli.js index ddd85cc..e682d04 100755 --- a/src/cli.js +++ b/src/cli.js @@ -138,6 +138,8 @@ const createEndpoints = async (destDir, lang, config) => { 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(query)); diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 5d9b8ec..05f99f3 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -34,19 +34,26 @@ endpoints.forEach(function(endpoint) { const hasGetMany = method.name === 'get_list' const pythonMethodName = generate_method_name(method.name, path) - let queries = [] + const queriesWithNames = [] if (method.query) { - queries.push(method.query) + queriesWithNames.push({ "result" : method.query }) + } else if (method.aggregated_queries) { + for (const [key, value] of Object.entries(method.aggregated_queries)) { + queriesWithNames.push({ [key]: value }) + } } - queries = queries.map(query => { - const sql = convertToPsycopgNamedArguments(formatQuery(query)) - const params = extractParamsFromQuery(query); - const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] - ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' - : '' - return ({ sql : sql, formattedParams: formattedParams }) + const queries = [] + queriesWithNames.forEach(queryWithName => { + for (const [name, query] of Object.entries(queryWithName)) { + const sql = convertToPsycopgNamedArguments(formatQuery(query)) + const params = extractParamsFromQuery(query); + const formattedParams = params.length > 0 + // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] + ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' + : '' + queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) + } }) if (hasGetOne || hasGetMany) { @@ -62,14 +69,29 @@ def <%- pythonMethodName %>(<%- paramsFromPath.join(', ') %>): port = 5432) try: with conn: +<% if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> + with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: + result = {} +<% queries.forEach(queryInfo => { + for (const [name, query] of Object.entries(queryInfo)) { +-%> + cur.execute('<%- query.sql %>'<%- query.formattedParams %>) + result['<%- name %>'] = cur.fetchone()[0] +<% } + }) +-%> + return result +<% + } else { + const query = queries[0].result +-%> with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: -<% queries.forEach(query => { -%> cur.execute('<%- query.sql %>'<%- query.formattedParams %>) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) return result -<% }) -%> +<% } -%> finally: conn.close() <% From 574fe282c848af69f1e15bfde54a33473e52ac5d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 29 Sep 2022 11:06:35 +0700 Subject: [PATCH 083/346] chore(golang,js): don't fail on not yet supported aggregated_queries Part of #17 --- src/templates/routes.go.ejs | 5 +++++ src/templates/routes.js.ejs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 12a5fe6..96faaf9 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -143,6 +143,7 @@ function dtoInCache(dto) { const verbs_with_dto = [ 'get', 'post', 'put' ] endpoints.forEach(function(endpoint) { const dtos = endpoint.methods + .filter(method => method.query) // filter out aggregated_queries for a while (see #17) .filter(method => verbs_with_dto.includes(method.verb)) .map(method => query2dto(sqlParser, method)) .filter(elem => elem) // filter out nulls @@ -161,6 +162,10 @@ endpoints.forEach(function(endpoint) { const path = convertPathPlaceholders(endpoint.path); endpoint.methods.forEach(function(method) { + if (!method.query) { + // filter out aggregated_queries for a while (see #17) + return + } const params = extractParamsFromQuery(method.query); const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index d905f54..e8ca6ea 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -5,6 +5,10 @@ endpoints.forEach(function(endpoint) { const path = endpoint.path; endpoint.methods.forEach(function(method) { + if (!method.query) { + // filter out aggregated_queries for a while (see #17) + return + } const hasGetOne = method.name === 'get'; const hasGetMany = method.name === 'get_list'; const sql = formatQuery(method.query); From edb72c8d8d0f50fc17b225eadc506e015f26b349 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 29 Sep 2022 11:11:37 +0700 Subject: [PATCH 084/346] chore: update comments and replace TODO by LATER in order to not puzzle 0pdd bot --- src/templates/routes.go.ejs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 96faaf9..73d4332 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -47,12 +47,12 @@ function extractInsertValues(queryAst) { // ] // } => [ 'user_id' ] function extractUpdateValues(queryAst) { - // TODO: distinguish between b.param and q.param and extract only first + // LATER: distinguish between b.param and q.param and extract only first return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null) .filter(value => value) // filter out nulls } -// TODO: consider taking into account b.params from WHERE clause +// LATER: consider taking into account b.params from WHERE clause function extractProperties(queryAst) { if (queryAst.type === 'select') { return extractSelectParameters(queryAst); @@ -109,7 +109,7 @@ function query2dto(parser, method) { "maxFieldNameLength": lengthOfLongestString(props.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))), // required for de-duplication // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" - // TODO: sort before join + // LATER: sort before join "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') }; } @@ -171,14 +171,14 @@ endpoints.forEach(function(endpoint) { const hasGetMany = method.name === 'get_list'; if (hasGetOne || hasGetMany) { const dto = query2dto(sqlParser, method); - // TODO: do we really need signature and cache? + // LATER: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; const dataType = hasGetMany ? '[]' + dtoName : dtoName; const queryFunction = hasGetOne ? 'Get' : 'Select'; - // TODO: handle only particular method (get/post/put) - // TODO: include method/path into an error message + // LATER: handle only particular method (get/post/put) + // LATER: include method/path into an error message %> r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { <% @@ -215,7 +215,7 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'post') { const dto = query2dto(sqlParser, method); - // TODO: do we really need signature and cache? + // LATER: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; %> @@ -242,7 +242,7 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'put') { const dto = query2dto(sqlParser, method); - // TODO: do we really need signature and cache? + // LATER: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; %> From 52a80c7fcaae62a65eaf3246260aa059e8239516 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 29 Sep 2022 11:12:55 +0700 Subject: [PATCH 085/346] refactor(python): extract database connection code to a shared method See https://fastapi.tiangolo.com/tutorial/dependencies/ Part of #16 --- examples/python/routes.py | 38 +++++++++++-------------------------- src/templates/routes.py.ejs | 21 +++++++++++--------- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 2b31089..48f4263 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -2,19 +2,21 @@ import psycopg2 import psycopg2.extras -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, Depends, HTTPException router = APIRouter() - -@router.get('/v1/categories/count') -def get_v1_categories_count(): - conn = psycopg2.connect( +async def db_connection(): + return psycopg2.connect( database = os.getenv('DB_NAME'), user = os.getenv('DB_USER'), password = os.getenv('DB_PASSWORD'), host = os.getenv('DB_HOST', 'localhost'), port = 5432) + + +@router.get('/v1/categories/count') +def get_v1_categories_count(conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: @@ -27,13 +29,7 @@ def get_v1_categories_count(): conn.close() @router.get('/v1/categories/stat') -def get_v1_categories_stat(): - conn = psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) +def get_v1_categories_stat(conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: @@ -51,13 +47,7 @@ def get_v1_categories_stat(): conn.close() @router.get('/v1/collections/{collectionId}/categories/count') -def get_v1_collections_collection_id_categories_count(collectionId): - conn = psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) +def get_v1_collections_collection_id_categories_count(collectionId, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: @@ -70,7 +60,7 @@ def get_v1_collections_collection_id_categories_count(collectionId): conn.close() @router.get('/v1/categories') -def get_list_v1_categories(): +def get_list_v1_categories(conn = Depends(db_connection)): pass @router.post('/v1/categories') @@ -78,13 +68,7 @@ def post_v1_categories(): pass @router.get('/v1/categories/{categoryId}') -def get_v1_categories_category_id(categoryId): - conn = psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) +def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 05f99f3..00c1150 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -2,10 +2,18 @@ import os import psycopg2 import psycopg2.extras -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, Depends, HTTPException router = APIRouter() +async def db_connection(): + return psycopg2.connect( + database = os.getenv('DB_NAME'), + user = os.getenv('DB_USER'), + password = os.getenv('DB_PASSWORD'), + host = os.getenv('DB_HOST', 'localhost'), + port = 5432) + <% // { "get", "/v1/categories/:categoryId" } => "get_v1_categories_category_id" function generate_method_name(method, path) { @@ -27,7 +35,8 @@ function convertToFastApiPath(path) { endpoints.forEach(function(endpoint) { const path = convertToFastApiPath(endpoint.path) - const paramsFromPath = extractParamsFromPath(endpoint.path) + const methodArgs = extractParamsFromPath(endpoint.path) + methodArgs.push('conn = Depends(db_connection)') endpoint.methods.forEach(function(method) { const hasGetOne = method.name === 'get' @@ -59,14 +68,8 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> @router.get('<%- path %>') -def <%- pythonMethodName %>(<%- paramsFromPath.join(', ') %>): +def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% if (hasGetOne) { -%> - conn = psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) try: with conn: <% if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> From 3b425e8226c74ee3d0da95c41a6fd3cbbd2b0708 Mon Sep 17 00:00:00 2001 From: Nikita Kirsanov Date: Wed, 5 Oct 2022 17:30:55 +0700 Subject: [PATCH 086/346] fix(python): use double quotes around query Co-authored-by: kirsanium --- examples/python/routes.py | 14 +++++++------- src/templates/routes.py.ejs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 48f4263..e87073e 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -20,7 +20,7 @@ def get_v1_categories_count(conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute('SELECT COUNT(*) AS counter FROM categories') + cur.execute("SELECT COUNT(*) AS counter FROM categories") result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -34,13 +34,13 @@ def get_v1_categories_stat(conn = Depends(db_connection)): with conn: with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: result = {} - cur.execute('SELECT COUNT(*) FROM categories') + cur.execute("SELECT COUNT(*) FROM categories") result['total'] = cur.fetchone()[0] - cur.execute('SELECT COUNT(*) FROM categories WHERE name_ru IS NOT NULL') + cur.execute("SELECT COUNT(*) FROM categories WHERE name_ru IS NOT NULL") result['in_russian'] = cur.fetchone()[0] - cur.execute('SELECT COUNT(*) FROM categories WHERE name IS NOT NULL') + cur.execute("SELECT COUNT(*) FROM categories WHERE name IS NOT NULL") result['in_english'] = cur.fetchone()[0] - cur.execute('SELECT COUNT(*) FROM categories WHERE name IS NOT NULL AND name_ru IS NOT NULL') + cur.execute("SELECT COUNT(*) FROM categories WHERE name IS NOT NULL AND name_ru IS NOT NULL") result['fully_translated'] = cur.fetchone()[0] return result finally: @@ -51,7 +51,7 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute('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)s', { "collectionId": collectionId }) + cur.execute("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)s", { "collectionId": collectionId }) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -72,7 +72,7 @@ def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute('SELECT id , name , name_ru , slug FROM categories WHERE id = %(categoryId)s', { "categoryId": categoryId }) + cur.execute("SELECT id , name , name_ru , slug FROM categories WHERE id = %(categoryId)s", { "categoryId": categoryId }) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 00c1150..fcfecce 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -78,7 +78,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% queries.forEach(queryInfo => { for (const [name, query] of Object.entries(queryInfo)) { -%> - cur.execute('<%- query.sql %>'<%- query.formattedParams %>) + cur.execute("<%- query.sql %>"<%- query.formattedParams %>) result['<%- name %>'] = cur.fetchone()[0] <% } }) @@ -89,7 +89,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const query = queries[0].result -%> with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute('<%- query.sql %>'<%- query.formattedParams %>) + cur.execute("<%- query.sql %>"<%- query.formattedParams %>) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) From adb50a243f1c3ba87b3798512175cbba99741c93 Mon Sep 17 00:00:00 2001 From: kirsanium Date: Wed, 5 Oct 2022 15:56:30 +0700 Subject: [PATCH 087/346] fix(python): correctly handle type casts --- src/templates/routes.py.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index fcfecce..7c13df2 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -24,7 +24,7 @@ function generate_method_name(method, path) { // "INSERT INTO ... VALUES(:categoryId)" => "INSERT INTO ... VALUES(%(categoryId)s)" // See: https://www.psycopg.org/docs/usage.html#passing-parameters-to-sql-queries function convertToPsycopgNamedArguments(sql) { - return sql.replace(/:([_a-zA-Z]+)/g, '%($1)s') + return sql.replace(/(? "/categories/{categoryId}" From b9c08ce20633878e058eeaac6b4d0ef33883df1f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 6 Oct 2022 10:22:09 +0700 Subject: [PATCH 088/346] feat(python): implement get_list Part of #16 --- examples/python/routes.py | 8 +++++++- src/templates/routes.py.ejs | 18 +++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index e87073e..2035404 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -61,7 +61,13 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen @router.get('/v1/categories') def get_list_v1_categories(conn = Depends(db_connection)): - pass + try: + with conn: + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + cur.execute("SELECT id , name , name_ru , slug FROM categories") + return cur.fetchall() + finally: + conn.close() @router.post('/v1/categories') def post_v1_categories(): diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 7c13df2..947de46 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -69,10 +69,10 @@ endpoints.forEach(function(endpoint) { %> @router.get('<%- path %>') def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): -<% if (hasGetOne) { -%> try: with conn: -<% if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> +<% if (hasGetOne) { + if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: result = {} <% queries.forEach(queryInfo => { @@ -94,15 +94,19 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): if result is None: raise HTTPException(status_code=404) return result -<% } -%> - finally: - conn.close() -<% +<% } } else { + const query = queries[0].result -%> - pass + with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + cur.execute("<%- query.sql %>"<%- query.formattedParams %>) + return cur.fetchall() <% } +-%> + finally: + conn.close() +<% } if (method.name === 'post') { %> From f524f15523d423024feadc1234f96595d159ce3d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Oct 2022 10:16:16 +0700 Subject: [PATCH 089/346] feat: add support for using query parameters Fix #23 --- README.md | 3 ++- examples/go/routes.go | 12 +++++++++++- examples/js/endpoints.yaml | 3 ++- examples/js/routes.js | 3 ++- examples/python/routes.py | 4 ++-- src/cli.js | 16 +++++++++++++--- src/templates/routes.go.ejs | 2 +- src/templates/routes.py.ejs | 7 +++++-- 8 files changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 89724dc..01a7bb1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) query: >- SELECT id, name, name_ru, slug FROM categories + LIMIT :q.limit post: query: >- INSERT INTO categories(name, slug, created_at, created_by, updated_at, updated_by) @@ -41,7 +42,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) FROM categories WHERE id = :p.categoryId ``` - Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. + Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `q` (query), `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. 1. Generate code | Language | Command for code generation | Example of generated files | Libraries | diff --git a/examples/go/routes.go b/examples/go/routes.go index 0db1a33..c9e38ef 100644 --- a/examples/go/routes.go +++ b/examples/go/routes.go @@ -76,8 +76,18 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { + nstmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories LIMIT :limit") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + var result []CategoryDto - err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories") + args := map[string]interface{}{ + "limit": r.URL.Query().Get("limit"), + } + err = nstmt.Get(&result, args) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 7d1d09a..1eb9026 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -35,7 +35,8 @@ , name , name_ru , slug - FROM categories + FROM categories + LIMIT :q.limit dto: name: CategoryDto fields: diff --git a/examples/js/routes.js b/examples/js/routes.js index a6d39a3..c3bfe20 100644 --- a/examples/js/routes.js +++ b/examples/js/routes.js @@ -36,7 +36,8 @@ app.get('/v1/collections/:collectionId/categories/count', (req, res) => { app.get('/v1/categories', (req, res) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories', + 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', + { "limit": req.query.limit }, (err, rows, fields) => { if (err) { throw err diff --git a/examples/python/routes.py b/examples/python/routes.py index 2035404..aa4842d 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -60,11 +60,11 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen conn.close() @router.get('/v1/categories') -def get_list_v1_categories(conn = Depends(db_connection)): +def get_list_v1_categories(limit, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("SELECT id , name , name_ru , slug FROM categories") + cur.execute("SELECT id , name , name_ru , slug FROM categories LIMIT %(limit)s", { "limit": limit }) return cur.fetchall() finally: conn.close() diff --git a/src/cli.js b/src/cli.js index e682d04..bf7936a 100755 --- a/src/cli.js +++ b/src/cli.js @@ -92,8 +92,8 @@ const createApp = async (destDir, lang) => { // "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, ''); +// "WHERE id = :p.categoryId OR id = :b.id LIMIT :q.limit" => "WHERE id = :categoryId OR id = :id LIMIT :limit" +const removePlaceholders = (query) => query.replace(/(?<=:)[pbq]\./g, ''); // "/categories/:id" => "/categories/{id}" // (used only with Golang's go-chi) @@ -152,6 +152,7 @@ const createEndpoints = async (destDir, lang, config) => { 'js': { 'p': 'req.params', 'b': 'req.body', + 'q': 'req.query', }, 'go': { 'p': function(param) { @@ -160,6 +161,9 @@ const createEndpoints = async (destDir, lang, config) => { 'b': function(param) { return 'dto.' + capitalize(snake2camelCase(param)); }, + 'q': function(param) { + return `r.URL.Query().Get("${param}")` + }, } } @@ -171,7 +175,11 @@ const createEndpoints = async (destDir, lang, config) => { "endpoints": config, // "... WHERE id = :p.id" => [ "p.id" ] - "extractParamsFromQuery": (query) => query.match(/(?<=:)[pb]\.\w+/g) || [], + "extractParamsFromQuery": (query) => query.match(/(?<=:)[pbq]\.\w+/g) || [], + + // "p.id" => "id" + the same for "q" and "b" + // (used only with FastAPI) + "stipOurPrefixes": (str) => str.replace(/^[pbq]\./, ''), // "/categories/:categoryId" => [ "categoryId" ] // (used only with FastAPI) @@ -230,6 +238,8 @@ const createEndpoints = async (destDir, lang, config) => { } ).join('\n\t\t\t'); }, + + "placeholdersMap": placeholdersMap, } ); diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 73d4332..f8d622d 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -193,7 +193,7 @@ endpoints.forEach(function(endpoint) { var result <%- dataType %> args := map[string]interface{}{ - <%- params.map(p => `"${p.substring(2)}": chi.URLParam(r, "${p.substring(2)}"),`).join('\n\t\t\t') %> + <%- /* LATER: extract */ params.map(p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)](p.substring(2))},`).join('\n\t\t\t') %> } err = nstmt.Get(&result, args) <% } else { -%> diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 947de46..4586908 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -35,14 +35,17 @@ function convertToFastApiPath(path) { endpoints.forEach(function(endpoint) { const path = convertToFastApiPath(endpoint.path) - const methodArgs = extractParamsFromPath(endpoint.path) - methodArgs.push('conn = Depends(db_connection)') + const argsFromPath = extractParamsFromPath(endpoint.path) endpoint.methods.forEach(function(method) { const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' const pythonMethodName = generate_method_name(method.name, path) + // LATER: add support for aggregated_queries (#17) + const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] + const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn = Depends(db_connection)'])) + const queriesWithNames = [] if (method.query) { queriesWithNames.push({ "result" : method.query }) From 131bca1c38ea3732c865e79931c32c79c5c7da23 Mon Sep 17 00:00:00 2001 From: kirsanium Date: Wed, 5 Oct 2022 21:23:21 +0700 Subject: [PATCH 090/346] feat: allow comments by removing them --- examples/js/endpoints.yaml | 15 +++++++++------ src/cli.js | 6 ++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index 1eb9026..f21b0e9 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -17,9 +17,12 @@ - path: /v1/collections/:collectionId/categories/count get: - query: >- + query: |- + -- comment SELECT COUNT(DISTINCT s.category_id) AS counter + -- comment2 FROM collections_series cs + -- comment3 JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :p.collectionId @@ -30,7 +33,7 @@ - path: /v1/categories get_list: - query: >- + query: |- SELECT id , name , name_ru @@ -43,7 +46,7 @@ id: type: integer post: - query: >- + query: |- INSERT INTO categories ( name @@ -68,7 +71,7 @@ - path: /v1/categories/:categoryId get: - query: >- + query: |- SELECT id , name , name_ru @@ -81,7 +84,7 @@ id: type: integer put: - query: >- + query: |- UPDATE categories SET name = :b.name , name_ru = :b.name_ru @@ -90,7 +93,7 @@ , updated_by = :b.user_id WHERE id = :p.categoryId delete: - query: >- + query: |- DELETE FROM categories WHERE id = :p.categoryId diff --git a/src/cli.js b/src/cli.js index bf7936a..a91615c 100755 --- a/src/cli.js +++ b/src/cli.js @@ -89,6 +89,8 @@ const createApp = async (destDir, lang) => { 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, ' '); @@ -142,7 +144,7 @@ const createEndpoints = async (destDir, lang, config) => { queries = Object.values(method.aggregated_queries) } queries.forEach(query => { - const sql = removePlaceholders(flattenQuery(query)); + const sql = removePlaceholders(flattenQuery(removeComments(query))); console.log(`\t${sql}`); }) }); @@ -204,7 +206,7 @@ const createEndpoints = async (destDir, lang, config) => { // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" "formatQuery": (query) => { - return removePlaceholders(flattenQuery(query)); + return removePlaceholders(flattenQuery(removeComments(query))); }, // (used only with Golang) From 304014cb3301ebf774a58a94797346364356c5fc Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 10 Oct 2022 10:28:45 +0700 Subject: [PATCH 091/346] chore: revert uneeded changes --- examples/js/endpoints.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index f21b0e9..a2267e1 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -33,7 +33,7 @@ - path: /v1/categories get_list: - query: |- + query: >- SELECT id , name , name_ru @@ -46,7 +46,7 @@ id: type: integer post: - query: |- + query: >- INSERT INTO categories ( name @@ -71,7 +71,7 @@ - path: /v1/categories/:categoryId get: - query: |- + query: >- SELECT id , name , name_ru @@ -84,7 +84,7 @@ id: type: integer put: - query: |- + query: >- UPDATE categories SET name = :b.name , name_ru = :b.name_ru @@ -93,7 +93,7 @@ , updated_by = :b.user_id WHERE id = :p.categoryId delete: - query: |- + query: >- DELETE FROM categories WHERE id = :p.categoryId From f176ea51be1e8c115400563ec6aa83b617f16336 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 10 Oct 2022 10:29:19 +0700 Subject: [PATCH 092/346] chore: add a comment to a method --- src/cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.js b/src/cli.js index a91615c..1d41ad7 100755 --- a/src/cli.js +++ b/src/cli.js @@ -89,6 +89,7 @@ const createApp = async (destDir, lang) => { fs.copyFileSync(`${__dirname}/templates/app.${ext}`, resultFile) }; +// "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" const removeComments = (query) => query.replace(/--.*\n/g, ''); // "SELECT *\n FROM foo" => "SELECT * FROM foo" From 6c05105e08c638e70e8ad2eaeee08ccb12924b30 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 10 Oct 2022 10:35:02 +0700 Subject: [PATCH 093/346] chore: modify comments --- examples/js/endpoints.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/js/endpoints.yaml b/examples/js/endpoints.yaml index a2267e1..4cd9448 100644 --- a/examples/js/endpoints.yaml +++ b/examples/js/endpoints.yaml @@ -18,11 +18,10 @@ - path: /v1/collections/:collectionId/categories/count get: query: |- - -- comment + -- Comments before query is allowed SELECT COUNT(DISTINCT s.category_id) AS counter - -- comment2 + -- ... as well as within a query FROM collections_series cs - -- comment3 JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :p.collectionId From ea976ac8ace05d0ebecb53fea46649e285824a7b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 09:32:41 +0700 Subject: [PATCH 094/346] feat(python): keep formatting and indentation of the multiline SQL queries Part of #26 --- examples/python/routes.py | 29 ++++++++++++++++++++++++++--- src/cli.js | 1 + src/templates/routes.py.ejs | 21 +++++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index aa4842d..c71b020 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -51,7 +51,14 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("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)s", { "collectionId": collectionId }) + cur.execute( + """ + 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)s + """, { "collectionId": collectionId }) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -64,7 +71,15 @@ def get_list_v1_categories(limit, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("SELECT id , name , name_ru , slug FROM categories LIMIT %(limit)s", { "limit": limit }) + cur.execute( + """ + SELECT id + , name + , name_ru + , slug + FROM categories + LIMIT %(limit)s + """, { "limit": limit }) return cur.fetchall() finally: conn.close() @@ -78,7 +93,15 @@ def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("SELECT id , name , name_ru , slug FROM categories WHERE id = %(categoryId)s", { "categoryId": categoryId }) + cur.execute( + """ + SELECT id + , name + , name_ru + , slug + FROM categories + WHERE id = %(categoryId)s + """, { "categoryId": categoryId }) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) diff --git a/src/cli.js b/src/cli.js index 1d41ad7..102f05f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -243,6 +243,7 @@ const createEndpoints = async (destDir, lang, config) => { }, "placeholdersMap": placeholdersMap, + "removeComments": removeComments, } ); diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 4586908..fd6b494 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -32,6 +32,19 @@ function convertToFastApiPath(path) { return path.replace(/:([_a-zA-Z]+)/g, '{$1}') } +// Differs from formatQuery() as it doesn't flatten query (preserve original formatting) +// and also use """ for multiline strings +function formatQueryForPython(query, indentLevel) { + const sql = removePlaceholders(removeComments(query)) + const isMultilineSql = sql.indexOf('\n') >= 0 + if (isMultilineSql) { + const indent = ' '.repeat(indentLevel) + const indentedSql = sql.replace(/\n/g, '\n' + indent) + return `\n${indent}"""\n${indent}${indentedSql}\n${indent}"""`; + } + return `"${sql}"` +} + endpoints.forEach(function(endpoint) { const path = convertToFastApiPath(endpoint.path) @@ -58,7 +71,7 @@ endpoints.forEach(function(endpoint) { const queries = [] queriesWithNames.forEach(queryWithName => { for (const [name, query] of Object.entries(queryWithName)) { - const sql = convertToPsycopgNamedArguments(formatQuery(query)) + const sql = convertToPsycopgNamedArguments(formatQueryForPython(query, 20)) const params = extractParamsFromQuery(query); const formattedParams = params.length > 0 // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] @@ -81,7 +94,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% queries.forEach(queryInfo => { for (const [name, query] of Object.entries(queryInfo)) { -%> - cur.execute("<%- query.sql %>"<%- query.formattedParams %>) + cur.execute(<%- query.sql %><%- query.formattedParams %>) result['<%- name %>'] = cur.fetchone()[0] <% } }) @@ -92,7 +105,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const query = queries[0].result -%> with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("<%- query.sql %>"<%- query.formattedParams %>) + cur.execute(<%- query.sql %><%- query.formattedParams %>) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -102,7 +115,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const query = queries[0].result -%> with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: - cur.execute("<%- query.sql %>"<%- query.formattedParams %>) + cur.execute(<%- query.sql %><%- query.formattedParams %>) return cur.fetchall() <% } From 213b506cea2f8f5bbfc916d6e59afae0e6824b67 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 09:43:52 +0700 Subject: [PATCH 095/346] feat(python): now generated python code passes flake8 --max-line-length 120 check Fix #29 --- examples/python/routes.py | 45 +++++++++++++++++++++---------------- src/templates/routes.py.ejs | 28 +++++++++++++---------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index c71b020..5ef27d6 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -6,20 +6,21 @@ router = APIRouter() + async def db_connection(): return psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) + database=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + host=os.getenv('DB_HOST', 'localhost'), + port=5432) @router.get('/v1/categories/count') -def get_v1_categories_count(conn = Depends(db_connection)): +def get_v1_categories_count(conn=Depends(db_connection)): try: with conn: - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute("SELECT COUNT(*) AS counter FROM categories") result = cur.fetchone() if result is None: @@ -28,11 +29,12 @@ def get_v1_categories_count(conn = Depends(db_connection)): finally: conn.close() + @router.get('/v1/categories/stat') -def get_v1_categories_stat(conn = Depends(db_connection)): +def get_v1_categories_stat(conn=Depends(db_connection)): try: with conn: - with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: result = {} cur.execute("SELECT COUNT(*) FROM categories") result['total'] = cur.fetchone()[0] @@ -46,11 +48,12 @@ def get_v1_categories_stat(conn = Depends(db_connection)): finally: conn.close() + @router.get('/v1/collections/{collectionId}/categories/count') -def get_v1_collections_collection_id_categories_count(collectionId, conn = Depends(db_connection)): +def get_v1_collections_collection_id_categories_count(collectionId, conn=Depends(db_connection)): try: with conn: - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute( """ SELECT COUNT(DISTINCT s.category_id) AS counter @@ -58,7 +61,7 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = %(collectionId)s - """, { "collectionId": collectionId }) + """, {"collectionId": collectionId}) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -66,11 +69,12 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn = Depen finally: conn.close() + @router.get('/v1/categories') -def get_list_v1_categories(limit, conn = Depends(db_connection)): +def get_list_v1_categories(limit, conn=Depends(db_connection)): try: with conn: - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute( """ SELECT id @@ -79,20 +83,22 @@ def get_list_v1_categories(limit, conn = Depends(db_connection)): , slug FROM categories LIMIT %(limit)s - """, { "limit": limit }) + """, {"limit": limit}) return cur.fetchall() finally: conn.close() + @router.post('/v1/categories') def post_v1_categories(): pass + @router.get('/v1/categories/{categoryId}') -def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): +def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): try: with conn: - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute( """ SELECT id @@ -101,7 +107,7 @@ def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): , slug FROM categories WHERE id = %(categoryId)s - """, { "categoryId": categoryId }) + """, {"categoryId": categoryId}) result = cur.fetchone() if result is None: raise HTTPException(status_code=404) @@ -109,11 +115,12 @@ def get_v1_categories_category_id(categoryId, conn = Depends(db_connection)): finally: conn.close() + @router.put('/v1/categories/{categoryId}') def put_v1_categories_category_id(): pass + @router.delete('/v1/categories/{categoryId}') def delete_v1_categories_category_id(): pass - diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index fd6b494..e3d7a00 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -6,14 +6,14 @@ from fastapi import APIRouter, Depends, HTTPException router = APIRouter() + async def db_connection(): return psycopg2.connect( - database = os.getenv('DB_NAME'), - user = os.getenv('DB_USER'), - password = os.getenv('DB_PASSWORD'), - host = os.getenv('DB_HOST', 'localhost'), - port = 5432) - + database=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + host=os.getenv('DB_HOST', 'localhost'), + port=5432) <% // { "get", "/v1/categories/:categoryId" } => "get_v1_categories_category_id" function generate_method_name(method, path) { @@ -57,7 +57,7 @@ endpoints.forEach(function(endpoint) { // LATER: add support for aggregated_queries (#17) const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] - const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn = Depends(db_connection)'])) + const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) const queriesWithNames = [] if (method.query) { @@ -75,7 +75,7 @@ endpoints.forEach(function(endpoint) { const params = extractParamsFromQuery(query); const formattedParams = params.length > 0 // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] - ? ', { ' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + ' }' + ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + '}' : '' queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) } @@ -83,13 +83,14 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> + @router.get('<%- path %>') def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): try: with conn: <% if (hasGetOne) { if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> - with conn.cursor(cursor_factory = psycopg2.extras.DictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: result = {} <% queries.forEach(queryInfo => { for (const [name, query] of Object.entries(queryInfo)) { @@ -104,7 +105,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } else { const query = queries[0].result -%> - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(<%- query.sql %><%- query.formattedParams %>) result = cur.fetchone() if result is None: @@ -114,7 +115,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } else { const query = queries[0].result -%> - with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(<%- query.sql %><%- query.formattedParams %>) return cur.fetchall() <% @@ -126,6 +127,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } if (method.name === 'post') { %> + @router.post('<%- path %>') def <%- pythonMethodName %>(): pass @@ -134,6 +136,7 @@ def <%- pythonMethodName %>(): } if (method.name === 'put') { %> + @router.put('<%- path %>') def <%- pythonMethodName %>(): pass @@ -142,6 +145,7 @@ def <%- pythonMethodName %>(): } if (method.name === 'delete') { %> + @router.delete('<%- path %>') def <%- pythonMethodName %>(): pass @@ -150,4 +154,4 @@ def <%- pythonMethodName %>(): } }) }) -%> +%> \ No newline at end of file From ed8f4ecd77c9f0f05d22375353eeae2981eb919f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 09:57:22 +0700 Subject: [PATCH 096/346] refactor: extract a method Part of #24 --- src/cli.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index 102f05f..716017f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -307,13 +307,16 @@ afteward to run`) } }; +const absolutePathToDestDir = (argv) => { + const relativeDestDir = argv._.length > 0 ? argv._[0] : '.' + return path.resolve(process.cwd(), relativeDestDir) +} const argv = parseCommandLineArgs(process.argv.slice(2)); const config = loadConfig(endpointsFile); -let destDir = argv._.length > 0 ? argv._[0] : '.'; -destDir = path.resolve(process.cwd(), destDir); +const destDir = absolutePathToDestDir(argv) console.log('Destination directory:', destDir) if (!fs.existsSync(destDir)) { From 5f95e9de81eb4d83154bd0a69626387a99f74c8a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 10:01:40 +0700 Subject: [PATCH 097/346] feat: add --dest-dir option to specify a destination directory of generated files Example: $ query2app --lang python --dest-dir src $ find src 10:02 src src/requirements.txt src/app.py src/routes.py Fix #24 --- src/cli.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli.js b/src/cli.js index 716017f..f6d8168 100755 --- a/src/cli.js +++ b/src/cli.js @@ -13,9 +13,11 @@ const endpointsFile = 'endpoints.yaml'; const parseCommandLineArgs = (args) => { const opts = { - 'string': [ 'lang' ], + // @todo #24 Document --dest-dir option + 'string': [ 'lang', 'dest-dir' ], 'default': { - 'lang': 'js' + 'lang': 'js', + 'dest-dir': '.' } }; const argv = parseArgs(args, opts); @@ -268,6 +270,7 @@ const createDependenciesDescriptor = async (destDir, lang) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); + // @todo #24 [js] Possibly incorrect project name with --dest-dir option const projectName = path.basename(destDir); console.log('Project name:', projectName); @@ -308,7 +311,7 @@ afteward to run`) }; const absolutePathToDestDir = (argv) => { - const relativeDestDir = argv._.length > 0 ? argv._[0] : '.' + const relativeDestDir = argv._.length > 0 ? argv._[0] : argv['dest-dir'] return path.resolve(process.cwd(), relativeDestDir) } From 37994857f73422f582d6c8fc3909272cb16da268 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 10:24:25 +0700 Subject: [PATCH 098/346] chore(golang,python): don't print project name (as it's useful only for js) --- src/cli.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index f6d8168..249e67b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -272,7 +272,9 @@ const createDependenciesDescriptor = async (destDir, lang) => { const resultFile = path.join(destDir, fileName); // @todo #24 [js] Possibly incorrect project name with --dest-dir option const projectName = path.basename(destDir); - console.log('Project name:', projectName); + if (lang === 'js') { + console.log('Project name:', projectName); + } const minimalPackageJson = await ejs.renderFile( `${__dirname}/templates/${fileName}.ejs`, From 96f6b62d89b6010e92e4abac297b388c4a20eafd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 11:31:48 +0700 Subject: [PATCH 099/346] feat(python): extract database initialisation logic from routes.py to db.py Fix #28 --- README.md | 2 +- examples/python/db.py | 11 +++++++++++ examples/python/routes.py | 12 ++---------- src/cli.js | 15 ++++++++++++++- src/templates/db.py | 11 +++++++++++ src/templates/routes.py.ejs | 12 ++---------- 6 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 examples/python/db.py create mode 100644 src/templates/db.py diff --git a/README.md b/README.md index 01a7bb1..1465d1e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`db.py`](examples/python/db.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application | Language | Commands to run the application | diff --git a/examples/python/db.py b/examples/python/db.py new file mode 100644 index 0000000..be8065b --- /dev/null +++ b/examples/python/db.py @@ -0,0 +1,11 @@ +import os +import psycopg2 + + +async def db_connection(): + return psycopg2.connect( + database=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + host=os.getenv('DB_HOST', 'localhost'), + port=5432) diff --git a/examples/python/routes.py b/examples/python/routes.py index 5ef27d6..459e029 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -1,19 +1,11 @@ -import os import psycopg2 import psycopg2.extras from fastapi import APIRouter, Depends, HTTPException -router = APIRouter() - +from db import db_connection -async def db_connection(): - return psycopg2.connect( - database=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD'), - host=os.getenv('DB_HOST', 'localhost'), - port=5432) +router = APIRouter() @router.get('/v1/categories/count') diff --git a/src/cli.js b/src/cli.js index 249e67b..d005953 100755 --- a/src/cli.js +++ b/src/cli.js @@ -88,9 +88,21 @@ const createApp = async (destDir, lang) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); - fs.copyFileSync(`${__dirname}/templates/app.${ext}`, resultFile) + fs.copyFileSync(`${__dirname}/templates/${fileName}`, resultFile) }; +const createDb = async (destDir, lang) => { + if (lang !== 'python') { + return + } + const fileName = 'db.py' + console.log('Generate', fileName); + const resultFile = path.join(destDir, fileName); + + // @todo #28 Create db.py with async API + fs.copyFileSync(`${__dirname}/templates/${fileName}`, resultFile) +} + // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" const removeComments = (query) => query.replace(/--.*\n/g, ''); @@ -330,6 +342,7 @@ if (!fs.existsSync(destDir)) { } createApp(destDir, argv.lang, config); +createDb(destDir, argv.lang) createEndpoints(destDir, argv.lang, config); createDependenciesDescriptor(destDir, argv.lang); showInstructions(argv.lang); diff --git a/src/templates/db.py b/src/templates/db.py new file mode 100644 index 0000000..be8065b --- /dev/null +++ b/src/templates/db.py @@ -0,0 +1,11 @@ +import os +import psycopg2 + + +async def db_connection(): + return psycopg2.connect( + database=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + host=os.getenv('DB_HOST', 'localhost'), + port=5432) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index e3d7a00..2a85f21 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -1,19 +1,11 @@ -import os import psycopg2 import psycopg2.extras from fastapi import APIRouter, Depends, HTTPException -router = APIRouter() - +from db import db_connection -async def db_connection(): - return psycopg2.connect( - database=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD'), - host=os.getenv('DB_HOST', 'localhost'), - port=5432) +router = APIRouter() <% // { "get", "/v1/categories/:categoryId" } => "get_v1_categories_category_id" function generate_method_name(method, path) { From 47b76aa86c5a7547772532daa716c7c560872e5f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Oct 2022 11:54:42 +0700 Subject: [PATCH 100/346] chore(golang,python): update instructions to export env vars for database connection --- README.md | 2 +- src/cli.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1465d1e..3c9067b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Commands to run the application | | -----------| --------------------------------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| - | Golang |
$ go run *.go
or
$ go build -o app
$ ./app
| + | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app
| --- diff --git a/src/cli.js b/src/cli.js index d005953..1c81944 100755 --- a/src/cli.js +++ b/src/cli.js @@ -310,15 +310,18 @@ to install its dependencies and afteward to run`); } else if (lang === 'go') { console.info(`Use + export DB_NAME=db DB_USER=user DB_PASSWORD=secret go run *.go or go build -o app + export DB_NAME=db DB_USER=user DB_PASSWORD=secret ./app to build and run it`) } else if (lang === 'python') { console.info(`Use pip install -r requirements.txt to install its dependencies and + export DB_NAME=db DB_USER=user DB_PASSWORD=secret uvicorn app:app afteward to run`) } From ef147bee133c9dacdabe3998fe0f86fa6acdb447 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Oct 2022 11:05:04 +0700 Subject: [PATCH 101/346] ci: add no-op workflows for testing generation of apps on different languages Part of #13 Part of #30 --- .github/generate-go-app.yml | 15 +++++++++++++++ .github/generate-js-app.yml | 15 +++++++++++++++ .github/generate-python-app.yml | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .github/generate-go-app.yml create mode 100644 .github/generate-js-app.yml create mode 100644 .github/generate-python-app.yml diff --git a/.github/generate-go-app.yml b/.github/generate-go-app.yml new file mode 100644 index 0000000..4a51ded --- /dev/null +++ b/.github/generate-go-app.yml @@ -0,0 +1,15 @@ +name: Generate Golang app + +on: + push: + +jobs: + run-checkstyle: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-20.04 + steps: + - name: Clone source code + uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + with: + # Whether to configure the token or SSH key with the local git config. Default: true + persist-credentials: false diff --git a/.github/generate-js-app.yml b/.github/generate-js-app.yml new file mode 100644 index 0000000..4a804eb --- /dev/null +++ b/.github/generate-js-app.yml @@ -0,0 +1,15 @@ +name: Generate JavaScript app + +on: + push: + +jobs: + run-checkstyle: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-20.04 + steps: + - name: Clone source code + uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + with: + # Whether to configure the token or SSH key with the local git config. Default: true + persist-credentials: false diff --git a/.github/generate-python-app.yml b/.github/generate-python-app.yml new file mode 100644 index 0000000..3c4f5a7 --- /dev/null +++ b/.github/generate-python-app.yml @@ -0,0 +1,15 @@ +name: Generate Python app + +on: + push: + +jobs: + run-checkstyle: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-20.04 + steps: + - name: Clone source code + uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + with: + # Whether to configure the token or SSH key with the local git config. Default: true + persist-credentials: false From c8d0c390217af571aa1d591cf75404f73eb8322a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Oct 2022 11:31:03 +0700 Subject: [PATCH 102/346] refactor: generate app file from EJS template Part of #27 --- src/cli.js | 8 +++++++- src/templates/{app.go => app.go.ejs} | 0 src/templates/{app.js => app.js.ejs} | 0 src/templates/{app.py => app.py.ejs} | 0 4 files changed, 7 insertions(+), 1 deletion(-) rename src/templates/{app.go => app.go.ejs} (100%) rename src/templates/{app.js => app.js.ejs} (100%) rename src/templates/{app.py => app.py.ejs} (100%) diff --git a/src/cli.js b/src/cli.js index 1c81944..097df80 100755 --- a/src/cli.js +++ b/src/cli.js @@ -88,7 +88,13 @@ const createApp = async (destDir, lang) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); - fs.copyFileSync(`${__dirname}/templates/${fileName}`, resultFile) + const resultedCode = await ejs.renderFile( + `${__dirname}/templates/${fileName}.ejs`, + { + } + ) + + fs.writeFileSync(resultFile, resultedCode); }; const createDb = async (destDir, lang) => { diff --git a/src/templates/app.go b/src/templates/app.go.ejs similarity index 100% rename from src/templates/app.go rename to src/templates/app.go.ejs diff --git a/src/templates/app.js b/src/templates/app.js.ejs similarity index 100% rename from src/templates/app.js rename to src/templates/app.js.ejs diff --git a/src/templates/app.py b/src/templates/app.py.ejs similarity index 100% rename from src/templates/app.py rename to src/templates/app.py.ejs From 595c1907722c80931051fb05a26ea71cc89fd234 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Oct 2022 11:52:56 +0700 Subject: [PATCH 103/346] feat(python): include user defined routes from *_routes.py` files In order to include custom routes: - create a file _routes.py - implement endpoints and ensure that the file contains FastAPI's APIRouter named "router" Part of #27 --- examples/python/app.py | 4 ++++ examples/python/custom_routes.py | 8 ++++++++ src/cli.js | 10 ++++++++++ src/templates/app.py.ejs | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 examples/python/custom_routes.py diff --git a/examples/python/app.py b/examples/python/app.py index 0043406..4ad6909 100644 --- a/examples/python/app.py +++ b/examples/python/app.py @@ -1,6 +1,10 @@ from fastapi import FastAPI from routes import router +from custom_routes import router as custom_route + app = FastAPI() app.include_router(router) + +app.include_router(custom_route) diff --git a/examples/python/custom_routes.py b/examples/python/custom_routes.py new file mode 100644 index 0000000..80b85a3 --- /dev/null +++ b/examples/python/custom_routes.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get('/v1/hello') +def greetings(): + return {"hello": "world!"} diff --git a/src/cli.js b/src/cli.js index 097df80..21941b3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -82,15 +82,25 @@ const lang2extension = (lang) => { } } +const findFileNamesEndWith = (dir, postfix) => { + return fs.readdirSync(dir).filter(name => name.endsWith(postfix)) +} + const createApp = async (destDir, lang) => { const ext = lang2extension(lang) const fileName = `app.${ext}` console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); + const customRouters = findFileNamesEndWith(destDir, `_routes.${ext}`) + if (customRouters.length > 0) { + customRouters.forEach(filename => console.log(`Include a custom router from ${filename}`)) + } const resultedCode = await ejs.renderFile( `${__dirname}/templates/${fileName}.ejs`, { + // @todo #27 Document usage of user defined routes + 'customRouteFilenames': customRouters } ) diff --git a/src/templates/app.py.ejs b/src/templates/app.py.ejs index 0043406..466996e 100644 --- a/src/templates/app.py.ejs +++ b/src/templates/app.py.ejs @@ -1,6 +1,25 @@ +<% + +// "custom_routes.py" => "custom_route" +function fileName2routerName(filename) { + return filename.replace(/_routes\.py$/, '_route') +} + +// "custom_routes.py" => "custom_routes" +function removeExtension(filename) { + return filename.replace(/\.py$/, '') +} + +-%> from fastapi import FastAPI from routes import router +<% customRouteFilenames.forEach(filename => { %> +from <%= removeExtension(filename) %> import router as <%= fileName2routerName(filename) %> +<% }) -%> app = FastAPI() app.include_router(router) +<% customRouteFilenames.forEach(filename => { %> +app.include_router(<%= fileName2routerName(filename) %>) +<% }) -%> \ No newline at end of file From f6a14fd0657012c7a957dd1f6d7c5abea0e12b0a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Oct 2022 12:16:32 +0700 Subject: [PATCH 104/346] ci: move workflow files to the appropriate directory Correction for ef147bee133c9dacdabe3998fe0f86fa6acdb447 commit. Part of #13 Part of #30 --- .github/{ => workflows}/generate-go-app.yml | 0 .github/{ => workflows}/generate-js-app.yml | 0 .github/{ => workflows}/generate-python-app.yml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/generate-go-app.yml (100%) rename .github/{ => workflows}/generate-js-app.yml (100%) rename .github/{ => workflows}/generate-python-app.yml (100%) diff --git a/.github/generate-go-app.yml b/.github/workflows/generate-go-app.yml similarity index 100% rename from .github/generate-go-app.yml rename to .github/workflows/generate-go-app.yml diff --git a/.github/generate-js-app.yml b/.github/workflows/generate-js-app.yml similarity index 100% rename from .github/generate-js-app.yml rename to .github/workflows/generate-js-app.yml diff --git a/.github/generate-python-app.yml b/.github/workflows/generate-python-app.yml similarity index 100% rename from .github/generate-python-app.yml rename to .github/workflows/generate-python-app.yml From 27ffed4faa098b8ba006c51b0a3fc8db5552eb7d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 14 Oct 2022 12:19:26 +0700 Subject: [PATCH 105/346] ci: correct job's name Correction for ef147bee133c9dacdabe3998fe0f86fa6acdb447 commit. Part of #13 Part of #30 --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 4a51ded..7fb78e2 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -4,7 +4,7 @@ on: push: jobs: - run-checkstyle: + generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 4a804eb..93a4c45 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -4,7 +4,7 @@ on: push: jobs: - run-checkstyle: + generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 3c4f5a7..9f9d346 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -4,7 +4,7 @@ on: push: jobs: - run-checkstyle: + generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: From dad002418e076f95b59f5bcb45cca887b2149090 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 27 Oct 2022 20:44:42 +0700 Subject: [PATCH 106/346] chore(python): modify post, put and delete endpoints to return 204 status instead of 200 Part of #16 --- examples/python/routes.py | 8 +++++--- src/templates/routes.py.ejs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/python/routes.py b/examples/python/routes.py index 459e029..ea9ae28 100644 --- a/examples/python/routes.py +++ b/examples/python/routes.py @@ -3,6 +3,8 @@ from fastapi import APIRouter, Depends, HTTPException +from starlette import status + from db import db_connection router = APIRouter() @@ -81,7 +83,7 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)): conn.close() -@router.post('/v1/categories') +@router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) def post_v1_categories(): pass @@ -108,11 +110,11 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): conn.close() -@router.put('/v1/categories/{categoryId}') +@router.put('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) def put_v1_categories_category_id(): pass -@router.delete('/v1/categories/{categoryId}') +@router.delete('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) def delete_v1_categories_category_id(): pass diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 2a85f21..7099e7c 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -3,6 +3,8 @@ import psycopg2.extras from fastapi import APIRouter, Depends, HTTPException +from starlette import status + from db import db_connection router = APIRouter() @@ -120,7 +122,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): if (method.name === 'post') { %> -@router.post('<%- path %>') +@router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) def <%- pythonMethodName %>(): pass <% @@ -129,7 +131,7 @@ def <%- pythonMethodName %>(): if (method.name === 'put') { %> -@router.put('<%- path %>') +@router.put('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) def <%- pythonMethodName %>(): pass <% @@ -138,7 +140,7 @@ def <%- pythonMethodName %>(): if (method.name === 'delete') { %> -@router.delete('<%- path %>') +@router.delete('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) def <%- pythonMethodName %>(): pass <% From 9d6e73c79969b1d2c5f0e1713c392dd73fa82b8f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 27 Oct 2022 20:51:22 +0700 Subject: [PATCH 107/346] chore: add 0pdd configuration file [skip ci] --- .0pdd.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .0pdd.yml diff --git a/.0pdd.yml b/.0pdd.yml new file mode 100644 index 0000000..e82b6e7 --- /dev/null +++ b/.0pdd.yml @@ -0,0 +1,10 @@ +# 0pdd configuration file. +# See for details: https://github.com/yegor256/0pdd +errors: + - slava.semushin+0pdd@gmail.com +alerts: + suppress: + - on-scope +format: + - short-title + - title-length=120 From e520355ed46507738ef62c6bef5fd809373287af Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 27 Oct 2022 21:20:55 +0700 Subject: [PATCH 108/346] ci: setup NodeJS and generare application on Python Part of #13 --- .github/workflows/generate-python-app.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 9f9d346..6fe7aea 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -13,3 +13,14 @@ jobs: with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS + uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + with: + node-version: 16 + - name: Install project dependencies + run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate Python + FastAPI application + run: ../../src/cli.js --lang python + working-directory: examples/python + - name: Check whether files were modified + run: git status --short From 7353c683292ec0b4ee25254896ceaa4a06b4160f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 10:38:47 +0700 Subject: [PATCH 109/346] ci: enable caching See https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-data Part of #13 --- .github/workflows/generate-python-app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 6fe7aea..d39688f 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -17,6 +17,7 @@ jobs: uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: node-version: 16 + cache: 'npm' - name: Install project dependencies run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application From e352297206b9c1660a5f7769d305188496998193 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 10:40:24 +0700 Subject: [PATCH 110/346] chore: update package-lock.json file with a newer version of NodeJS to fix warnings on CI npm ci complained on CI: npm WARN old lockfile npm WARN old lockfile The package-lock.json file was created with an old version of npm, npm WARN old lockfile so supplemental metadata must be fetched from the registry. npm WARN old lockfile npm WARN old lockfile This is a one-time fix-up, please be patient... npm WARN old lockfile and I install a newer node version (18) and updated the lock file with the following command: $ nvm install --lts $ npm install --package-lock-only See https://stackoverflow.com/questions/68260784/npm-warn-old-lockfile-the-package-lock-json-file-was-created-with-an-old-version Part of #13 --- package-lock.json | 218 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5b0299e..c70c2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,224 @@ { "name": "query2app", "version": "0.0.2", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "query2app", + "version": "0.0.2", + "license": "GPL-2.0", + "dependencies": { + "ejs": "~3.1.3", + "js-yaml": "~3.14.0", + "minimist": "~1.2.5", + "node-sql-parser": "~3.0.4" + }, + "bin": { + "query2app": "src/cli.js" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/ejs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", + "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", + "hasInstallScript": true, + "dependencies": { + "jake": "^10.6.1" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/filelist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", + "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dependencies": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/node-sql-parser": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-3.0.4.tgz", + "integrity": "sha512-ANB/paC3ZdcvrbRdiuQFsTvrmgzPozDUueTivO0Iep82h6KVRoER2h/KeZPsG0umYtCzwGY6KNhvB8xtJ6B/Rw==", + "dependencies": { + "big-integer": "^1.6.48" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + } + }, "dependencies": { "ansi-styles": { "version": "3.2.1", From 3cb3b2e8af44403fc5be78f7ffd9557282ef18ef Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 10:43:46 +0700 Subject: [PATCH 111/346] ci: use the latest LTS version of NodeJS (18.x) Part of #13 --- .github/workflows/generate-python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index d39688f..d6bd939 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -16,7 +16,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: - node-version: 16 + node-version: 18 cache: 'npm' - name: Install project dependencies run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci From a6bf8e9681ebfcefbc5b6debd521efd4f5dfa4a3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 10:45:40 +0700 Subject: [PATCH 112/346] ci: use --dest-dir option instead of manually changing a working directory Part of #13 --- .github/workflows/generate-python-app.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index d6bd939..2573660 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,7 +21,6 @@ jobs: - name: Install project dependencies run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: ../../src/cli.js --lang python - working-directory: examples/python + run: ./src/cli.js --lang python --dest-dir examples/python - name: Check whether files were modified run: git status --short From 13a3d29045113cf7f5d08a7f840e67dd42286c0b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 10:58:08 +0700 Subject: [PATCH 113/346] ci: disable audit and fund during dependencies installation See: - https://docs.npmjs.com/cli/v8/commands/npm-ci#audit - https://docs.npmjs.com/cli/v8/commands/npm-ci#fund Part of #13 --- .github/workflows/generate-python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 2573660..125ae6a 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -19,7 +19,7 @@ jobs: node-version: 18 cache: 'npm' - name: Install project dependencies - run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci + run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application run: ./src/cli.js --lang python --dest-dir examples/python - name: Check whether files were modified From 701870193271e2ce956acb4d496c9d73ecf0b46c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 28 Oct 2022 11:02:50 +0700 Subject: [PATCH 114/346] revert: ci: use --dest-dir option instead of manually changing a working directory It fails as it can't find endpoints.yaml: Failed to parse endpoints.yaml: ENOENT: no such file or directory, open 'endpoints.yaml' This reverts commit a6bf8e9681ebfcefbc5b6debd521efd4f5dfa4a3. --- .github/workflows/generate-python-app.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 125ae6a..71ed644 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,6 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: ./src/cli.js --lang python --dest-dir examples/python + run: ../../src/cli.js --lang python + working-directory: examples/python - name: Check whether files were modified run: git status --short From f04375359beb846503270d7c042c8aeec506b470 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 09:59:15 +0700 Subject: [PATCH 115/346] ci: generate applications on JavaScript and Golang Part of #13 --- .github/workflows/generate-go-app.yml | 12 ++++++++++++ .github/workflows/generate-js-app.yml | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 7fb78e2..1401492 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -13,3 +13,15 @@ jobs: with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS + uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + with: + node-version: 18 + cache: 'npm' + - name: Install project dependencies + run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate Golang + Chi application + run: ../../src/cli.js --lang go + working-directory: examples/go + - name: Check whether files were modified + run: git status --short diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 93a4c45..1a95c0d 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -13,3 +13,15 @@ jobs: with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS + uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + with: + node-version: 18 + cache: 'npm' + - name: Install project dependencies + run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate JavaScript + Express application + run: ../../src/cli.js --lang js + working-directory: examples/js + - name: Check whether files were modified + run: git status --short From b945e8e9f01a8a2d009694649f75eee252416249 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 10:58:40 +0700 Subject: [PATCH 116/346] chore: add TODO comment Relate to #27 --- src/cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.js b/src/cli.js index 21941b3..853743f 100755 --- a/src/cli.js +++ b/src/cli.js @@ -100,6 +100,7 @@ const createApp = async (destDir, lang) => { `${__dirname}/templates/${fileName}.ejs`, { // @todo #27 Document usage of user defined routes + // @todo #27 Add integration test to ensure that custom router is picked up 'customRouteFilenames': customRouters } ) From 370df162539aab47cb229c980179c3e48164081e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 11:05:54 +0700 Subject: [PATCH 117/346] refactor: don't pass unused option to createApp() --- src/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index 853743f..e6cd1f5 100755 --- a/src/cli.js +++ b/src/cli.js @@ -361,7 +361,7 @@ if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, {recursive: true}); } -createApp(destDir, argv.lang, config); +createApp(destDir, argv.lang); createDb(destDir, argv.lang) createEndpoints(destDir, argv.lang, config); createDependenciesDescriptor(destDir, argv.lang); From 96c24c4deef5fef9cc95121f74dd5fe4ea2b8077 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 11:08:46 +0700 Subject: [PATCH 118/346] refactor: pass all options to the methods Part of #33 --- src/cli.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli.js b/src/cli.js index e6cd1f5..9dbbcef 100755 --- a/src/cli.js +++ b/src/cli.js @@ -86,7 +86,7 @@ const findFileNamesEndWith = (dir, postfix) => { return fs.readdirSync(dir).filter(name => name.endsWith(postfix)) } -const createApp = async (destDir, lang) => { +const createApp = async (destDir, { lang }) => { const ext = lang2extension(lang) const fileName = `app.${ext}` console.log('Generate', fileName); @@ -108,7 +108,7 @@ const createApp = async (destDir, lang) => { fs.writeFileSync(resultFile, resultedCode); }; -const createDb = async (destDir, lang) => { +const createDb = async (destDir, { lang }) => { if (lang !== 'python') { return } @@ -154,7 +154,7 @@ const lengthOfLongestString = (arr) => arr 0 /* initial value */ ); -const createEndpoints = async (destDir, lang, config) => { +const createEndpoints = async (destDir, { lang }, config) => { const ext = lang2extension(lang) const fileName = `routes.${ext}` console.log('Generate', fileName); @@ -281,7 +281,7 @@ const createEndpoints = async (destDir, lang, config) => { fs.writeFileSync(resultFile, resultedCode); }; -const createDependenciesDescriptor = async (destDir, lang) => { +const createDependenciesDescriptor = async (destDir, { lang }) => { let fileName; if (lang === 'js') { fileName = 'package.json' @@ -361,8 +361,8 @@ if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, {recursive: true}); } -createApp(destDir, argv.lang); -createDb(destDir, argv.lang) -createEndpoints(destDir, argv.lang, config); -createDependenciesDescriptor(destDir, argv.lang); +createApp(destDir, argv); +createDb(destDir, argv) +createEndpoints(destDir, argv, config); +createDependenciesDescriptor(destDir, argv); showInstructions(argv.lang); From c78fdfa937f71aa315f25e4f0ac594d1a59c4f01 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 11:34:07 +0700 Subject: [PATCH 119/346] feat!: don't overwrite files by default to preserve user's modifications and introduce --overwrite option to turn this off Fix #33 --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- src/cli.js | 49 ++++++++++++++++++----- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 1401492..83aa416 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Golang + Chi application - run: ../../src/cli.js --lang go + run: ../../src/cli.js --overwrite --lang go working-directory: examples/go - name: Check whether files were modified run: git status --short diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 1a95c0d..56b6645 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate JavaScript + Express application - run: ../../src/cli.js --lang js + run: ../../src/cli.js --overwrite --lang js working-directory: examples/js - name: Check whether files were modified run: git status --short diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 71ed644..7f0c40e 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: ../../src/cli.js --lang python + run: ../../src/cli.js --overwrite --lang python working-directory: examples/python - name: Check whether files were modified run: git status --short diff --git a/src/cli.js b/src/cli.js index 9dbbcef..410bbba 100755 --- a/src/cli.js +++ b/src/cli.js @@ -15,9 +15,12 @@ const parseCommandLineArgs = (args) => { const opts = { // @todo #24 Document --dest-dir option 'string': [ 'lang', 'dest-dir' ], + // @todo #33 Document --overwrite option + 'boolean': [ 'overwrite' ], 'default': { 'lang': 'js', - 'dest-dir': '.' + 'dest-dir': '.', + 'overwrite': false, } }; const argv = parseArgs(args, opts); @@ -86,7 +89,30 @@ const findFileNamesEndWith = (dir, postfix) => { return fs.readdirSync(dir).filter(name => name.endsWith(postfix)) } -const createApp = async (destDir, { lang }) => { +const stripPrefix = (text, prefix) => { + if (text.startsWith(prefix)) { + return text.slice(prefix.length) + } + return text +} + +const fileExistsHandler = (err) => { + if (err === null) { + // Success + return + } + //console.log(err) + if (err.code === 'EEXIST') { + // copyFile() puts original file name in err.path, so we use err.dest in that case + const filePath = err.dest || err.path + const file = stripPrefix(filePath, process.cwd() + '/') + console.warn(`WARNING: File ${file} already exists and won't be rewritten to preserve possible modifications. In order to overwrite the file, re-run the application with --overwrite option`) + return + } + throw err +} + +const createApp = async (destDir, { lang, overwrite }) => { const ext = lang2extension(lang) const fileName = `app.${ext}` console.log('Generate', fileName); @@ -105,10 +131,11 @@ const createApp = async (destDir, { lang }) => { } ) - fs.writeFileSync(resultFile, resultedCode); + const fsFlags = overwrite ? 'w' : 'wx' + await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) }; -const createDb = async (destDir, { lang }) => { +const createDb = async (destDir, { lang, overwrite }) => { if (lang !== 'python') { return } @@ -116,8 +143,8 @@ const createDb = async (destDir, { lang }) => { console.log('Generate', fileName); const resultFile = path.join(destDir, fileName); - // @todo #28 Create db.py with async API - fs.copyFileSync(`${__dirname}/templates/${fileName}`, resultFile) + const mode = overwrite ? 0 : fs.constants.COPYFILE_EXCL + await fs.copyFile(`${__dirname}/templates/${fileName}`, resultFile, mode, fileExistsHandler) } // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" @@ -154,7 +181,7 @@ const lengthOfLongestString = (arr) => arr 0 /* initial value */ ); -const createEndpoints = async (destDir, { lang }, config) => { +const createEndpoints = async (destDir, { lang, overwrite }, config) => { const ext = lang2extension(lang) const fileName = `routes.${ext}` console.log('Generate', fileName); @@ -278,10 +305,11 @@ const createEndpoints = async (destDir, { lang }, config) => { } ); - fs.writeFileSync(resultFile, resultedCode); + const fsFlags = overwrite ? 'w' : 'wx' + await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) }; -const createDependenciesDescriptor = async (destDir, { lang }) => { +const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { let fileName; if (lang === 'js') { fileName = 'package.json' @@ -313,7 +341,8 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { } ); - fs.writeFileSync(resultFile, minimalPackageJson); + const fsFlags = overwrite ? 'w' : 'wx' + await fs.writeFile(resultFile, minimalPackageJson, { 'flag': fsFlags }, fileExistsHandler) }; const showInstructions = (lang) => { From d45a754ca8400a27ed90a23608d382aa7735b6cd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 11:55:15 +0700 Subject: [PATCH 120/346] refactor: remove trailing semicolons from cli.js --- src/cli.js | 162 ++++++++++++++++++++++++++--------------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/src/cli.js b/src/cli.js index 410bbba..59e60ff 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,15 +1,15 @@ #!/usr/bin/env node -const yaml = require('js-yaml'); -const ejs = require('ejs'); -const fs = require('fs'); -const path = require('path'); +const yaml = require('js-yaml') +const ejs = require('ejs') +const fs = require('fs') +const path = require('path') -const parseArgs = require('minimist'); +const parseArgs = require('minimist') -const { Parser } = require('node-sql-parser'); +const { Parser } = require('node-sql-parser') -const endpointsFile = 'endpoints.yaml'; +const endpointsFile = 'endpoints.yaml' const parseCommandLineArgs = (args) => { const opts = { @@ -22,10 +22,10 @@ const parseCommandLineArgs = (args) => { 'dest-dir': '.', 'overwrite': false, } - }; - const argv = parseArgs(args, opts); - //console.debug('argv:', argv); - return argv; + } + const argv = parseArgs(args, opts) + //console.debug('argv:', argv) + return argv } // Restructure YAML configuration to simplify downstream code. @@ -44,34 +44,34 @@ const parseCommandLineArgs = (args) => { // } const restructureConfiguration = (config) => { for (const endpoint of config) { - endpoint.methods = []; + endpoint.methods = []; // this semicolon is really needed [ 'get', 'get_list', 'post', 'put', 'delete' ].forEach(method => { if (!endpoint.hasOwnProperty(method)) { - return; + return } endpoint.methods.push({ 'name': method, 'verb': method !== 'get_list' ? method : 'get', ...endpoint[method], - }); - delete endpoint[method]; - }); + }) + delete endpoint[method] + }) } -}; +} const loadConfig = (endpointsFile) => { - console.log('Read', endpointsFile); + console.log('Read', endpointsFile) try { - const content = fs.readFileSync(endpointsFile, 'utf8'); - const config = yaml.safeLoad(content); - restructureConfiguration(config); - //console.debug(config); - return config; + 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; + console.error(`Failed to parse ${endpointsFile}: ${ex.message}`) + throw ex } -}; +} const lang2extension = (lang) => { switch (lang) { @@ -115,8 +115,8 @@ const fileExistsHandler = (err) => { const createApp = async (destDir, { lang, overwrite }) => { const ext = lang2extension(lang) const fileName = `app.${ext}` - console.log('Generate', fileName); - const resultFile = path.join(destDir, fileName); + console.log('Generate', fileName) + const resultFile = path.join(destDir, fileName) const customRouters = findFileNamesEndWith(destDir, `_routes.${ext}`) if (customRouters.length > 0) { customRouters.forEach(filename => console.log(`Include a custom router from ${filename}`)) @@ -133,44 +133,44 @@ const createApp = async (destDir, { lang, overwrite }) => { const fsFlags = overwrite ? 'w' : 'wx' await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) -}; +} const createDb = async (destDir, { lang, overwrite }) => { if (lang !== 'python') { return } const fileName = 'db.py' - console.log('Generate', fileName); - const resultFile = path.join(destDir, fileName); + console.log('Generate', fileName) + const resultFile = path.join(destDir, fileName) const mode = overwrite ? 0 : fs.constants.COPYFILE_EXCL await fs.copyFile(`${__dirname}/templates/${fileName}`, resultFile, mode, fileExistsHandler) } // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" -const removeComments = (query) => query.replace(/--.*\n/g, ''); +const removeComments = (query) => query.replace(/--.*\n/g, '') // "SELECT *\n FROM foo" => "SELECT * FROM foo" -const flattenQuery = (query) => query.replace(/\n[ ]*/g, ' '); +const flattenQuery = (query) => query.replace(/\n[ ]*/g, ' ') // "WHERE id = :p.categoryId OR id = :b.id LIMIT :q.limit" => "WHERE id = :categoryId OR id = :id LIMIT :limit" -const removePlaceholders = (query) => query.replace(/(?<=:)[pbq]\./g, ''); +const removePlaceholders = (query) => query.replace(/(?<=:)[pbq]\./g, '') // "/categories/:id" => "/categories/{id}" // (used only with Golang's go-chi) -const convertPathPlaceholders = (path) => path.replace(/:([^\/]+)/g, '{$1}'); +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()); +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()); +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); +const capitalize = (str) => str[0].toUpperCase() + str.slice(1) // ["a", "bb", "ccc"] => 3 // (used only with Golang's go-chi) @@ -179,22 +179,22 @@ const lengthOfLongestString = (arr) => arr .reduce( (acc, val) => val > acc ? val : acc, 0 /* initial value */ - ); + ) const createEndpoints = async (destDir, { lang, overwrite }, config) => { const ext = lang2extension(lang) const fileName = `routes.${ext}` - console.log('Generate', fileName); - const resultFile = path.join(destDir, fileName); + console.log('Generate', fileName) + const resultFile = path.join(destDir, fileName) for (let endpoint of config) { - let path = endpoint.path; + let path = endpoint.path if (lang === 'go') { path = convertPathPlaceholders(path) } endpoint.methods.forEach(method => { - const verb = method.verb.toUpperCase(); - console.log(`${verb} ${path}`); + const verb = method.verb.toUpperCase() + console.log(`${verb} ${path}`) let queries = [] if (method.query) { @@ -203,10 +203,10 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { queries = Object.values(method.aggregated_queries) } queries.forEach(query => { - const sql = removePlaceholders(flattenQuery(removeComments(query))); - console.log(`\t${sql}`); + const sql = removePlaceholders(flattenQuery(removeComments(query))) + console.log(`\t${sql}`) }) - }); + }) } const placeholdersMap = { @@ -220,7 +220,7 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { return `chi.URLParam(r, "${param}")` }, 'b': function(param) { - return 'dto.' + capitalize(snake2camelCase(param)); + return 'dto.' + capitalize(snake2camelCase(param)) }, 'q': function(param) { return `r.URL.Query().Get("${param}")` @@ -228,7 +228,7 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { } } - const parser = new Parser(); + const parser = new Parser() const resultedCode = await ejs.renderFile( `${__dirname}/templates/routes.${ext}.ejs`, @@ -250,22 +250,22 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { // (used only with Express) "formatParamsAsJavaScriptObject": (params) => { if (params.length === 0) { - return params; + 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]; + const bindTarget = p.substring(0, 1) + const paramName = p.substring(2) + const prefix = placeholdersMap['js'][bindTarget] return `"${paramName}": ${prefix}.${paramName}` } - ).join(', '); + ).join(', ') }, // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" "formatQuery": (query) => { - return removePlaceholders(flattenQuery(removeComments(query))); + return removePlaceholders(flattenQuery(removeComments(query))) }, // (used only with Golang) @@ -283,34 +283,34 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { // (used only with Golang's go-chi) "formatParamsAsGolangMap": (params) => { if (params.length === 0) { - return params; + return params } - const maxParamNameLength = lengthOfLongestString(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 + '":'; + 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'); + ).join('\n\t\t\t') }, "placeholdersMap": placeholdersMap, "removeComments": removeComments, } - ); + ) const fsFlags = overwrite ? 'w' : 'wx' await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) -}; +} const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { - let fileName; + let fileName if (lang === 'js') { fileName = 'package.json' @@ -321,16 +321,16 @@ const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { fileName = 'requirements.txt' } else { - return; + return } - console.log('Generate', fileName); + console.log('Generate', fileName) - const resultFile = path.join(destDir, fileName); + const resultFile = path.join(destDir, fileName) // @todo #24 [js] Possibly incorrect project name with --dest-dir option - const projectName = path.basename(destDir); + const projectName = path.basename(destDir) if (lang === 'js') { - console.log('Project name:', projectName); + console.log('Project name:', projectName) } const minimalPackageJson = await ejs.renderFile( @@ -339,11 +339,11 @@ const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { // project name is being used only for package.json projectName } - ); + ) const fsFlags = overwrite ? 'w' : 'wx' await fs.writeFile(resultFile, minimalPackageJson, { 'flag': fsFlags }, fileExistsHandler) -}; +} const showInstructions = (lang) => { console.info('The application has been generated!') @@ -353,7 +353,7 @@ const showInstructions = (lang) => { to install its dependencies and export DB_NAME=db DB_USER=user DB_PASSWORD=secret npm start -afteward to run`); +afteward to run`) } else if (lang === 'go') { console.info(`Use export DB_NAME=db DB_USER=user DB_PASSWORD=secret @@ -371,27 +371,27 @@ to install its dependencies and uvicorn app:app afteward to run`) } -}; +} const absolutePathToDestDir = (argv) => { const relativeDestDir = argv._.length > 0 ? argv._[0] : argv['dest-dir'] return path.resolve(process.cwd(), relativeDestDir) } -const argv = parseCommandLineArgs(process.argv.slice(2)); +const argv = parseCommandLineArgs(process.argv.slice(2)) -const config = loadConfig(endpointsFile); +const config = loadConfig(endpointsFile) const destDir = absolutePathToDestDir(argv) console.log('Destination directory:', destDir) if (!fs.existsSync(destDir)) { console.log('Create', destDir) - fs.mkdirSync(destDir, {recursive: true}); + fs.mkdirSync(destDir, {recursive: true}) } -createApp(destDir, argv); +createApp(destDir, argv) createDb(destDir, argv) -createEndpoints(destDir, argv, config); -createDependenciesDescriptor(destDir, argv); -showInstructions(argv.lang); +createEndpoints(destDir, argv, config) +createDependenciesDescriptor(destDir, argv) +showInstructions(argv.lang) From a0493e78a5bf8d8eb1b5fcff78ff627125cfe6e8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 11:56:52 +0700 Subject: [PATCH 121/346] refactor: introduce main() function (in order to make it async later) --- src/cli.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cli.js b/src/cli.js index 59e60ff..e0a9ada 100755 --- a/src/cli.js +++ b/src/cli.js @@ -378,20 +378,24 @@ const absolutePathToDestDir = (argv) => { return path.resolve(process.cwd(), relativeDestDir) } -const argv = parseCommandLineArgs(process.argv.slice(2)) +const main = (argv) => { + const config = loadConfig(endpointsFile) -const config = loadConfig(endpointsFile) + const destDir = absolutePathToDestDir(argv) + console.log('Destination directory:', destDir) -const destDir = absolutePathToDestDir(argv) -console.log('Destination directory:', destDir) + if (!fs.existsSync(destDir)) { + console.log('Create', destDir) + fs.mkdirSync(destDir, {recursive: true}) + } -if (!fs.existsSync(destDir)) { - console.log('Create', destDir) - fs.mkdirSync(destDir, {recursive: true}) + createApp(destDir, argv) + createDb(destDir, argv) + createEndpoints(destDir, argv, config) + createDependenciesDescriptor(destDir, argv) + showInstructions(argv.lang) } -createApp(destDir, argv) -createDb(destDir, argv) -createEndpoints(destDir, argv, config) -createDependenciesDescriptor(destDir, argv) -showInstructions(argv.lang) + +const argv = parseCommandLineArgs(process.argv.slice(2)) +main(argv) From 30a8431ae73ebf8eb5a0db745170689a50b6ed5c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 7 Nov 2022 12:10:26 +0700 Subject: [PATCH 122/346] chore: move await-s to main() method Also use promisifed versions of the methods in order to make await really works. --- src/cli.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cli.js b/src/cli.js index e0a9ada..e00ca06 100755 --- a/src/cli.js +++ b/src/cli.js @@ -3,6 +3,7 @@ const yaml = require('js-yaml') const ejs = require('ejs') const fs = require('fs') +const fsPromises = require('fs/promises') const path = require('path') const parseArgs = require('minimist') @@ -132,7 +133,7 @@ const createApp = async (destDir, { lang, overwrite }) => { ) const fsFlags = overwrite ? 'w' : 'wx' - await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) + return fsPromises.writeFile(resultFile, resultedCode, { 'flag': fsFlags }).catch(fileExistsHandler) } const createDb = async (destDir, { lang, overwrite }) => { @@ -143,8 +144,8 @@ const createDb = async (destDir, { lang, overwrite }) => { console.log('Generate', fileName) const resultFile = path.join(destDir, fileName) - const mode = overwrite ? 0 : fs.constants.COPYFILE_EXCL - await fs.copyFile(`${__dirname}/templates/${fileName}`, resultFile, mode, fileExistsHandler) + const mode = overwrite ? 0 : fsPromises.constants.COPYFILE_EXCL + return fsPromises.copyFile(`${__dirname}/templates/${fileName}`, resultFile, mode).catch(fileExistsHandler) } // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" @@ -306,7 +307,7 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { ) const fsFlags = overwrite ? 'w' : 'wx' - await fs.writeFile(resultFile, resultedCode, { 'flag': fsFlags }, fileExistsHandler) + return fsPromises.writeFile(resultFile, resultedCode, { 'flag': fsFlags }).catch(fileExistsHandler) } const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { @@ -342,7 +343,7 @@ const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { ) const fsFlags = overwrite ? 'w' : 'wx' - await fs.writeFile(resultFile, minimalPackageJson, { 'flag': fsFlags }, fileExistsHandler) + return fsPromises.writeFile(resultFile, minimalPackageJson, { 'flag': fsFlags }).catch(fileExistsHandler) } const showInstructions = (lang) => { @@ -378,7 +379,7 @@ const absolutePathToDestDir = (argv) => { return path.resolve(process.cwd(), relativeDestDir) } -const main = (argv) => { +const main = async (argv) => { const config = loadConfig(endpointsFile) const destDir = absolutePathToDestDir(argv) @@ -389,10 +390,10 @@ const main = (argv) => { fs.mkdirSync(destDir, {recursive: true}) } - createApp(destDir, argv) - createDb(destDir, argv) - createEndpoints(destDir, argv, config) - createDependenciesDescriptor(destDir, argv) + await createApp(destDir, argv) + await createDb(destDir, argv) + await createEndpoints(destDir, argv, config) + await createDependenciesDescriptor(destDir, argv) showInstructions(argv.lang) } From 2b51906c16e95dd3b74e6be4a3247e15ba2f7e29 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 13 Nov 2022 15:27:59 +0700 Subject: [PATCH 123/346] revert: feat!: don't overwrite files by default to preserve user's modifications and introduce --overwrite option to turn this off This reverts commit c78fdfa937f71aa315f25e4f0ac594d1a59c4f01. Relate to #33 This also fixes #34 because the code that caused error has been removed. --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- src/cli.js | 48 +++++------------------ 4 files changed, 12 insertions(+), 42 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 83aa416..1401492 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Golang + Chi application - run: ../../src/cli.js --overwrite --lang go + run: ../../src/cli.js --lang go working-directory: examples/go - name: Check whether files were modified run: git status --short diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 56b6645..1a95c0d 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate JavaScript + Express application - run: ../../src/cli.js --overwrite --lang js + run: ../../src/cli.js --lang js working-directory: examples/js - name: Check whether files were modified run: git status --short diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 7f0c40e..71ed644 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: ../../src/cli.js --overwrite --lang python + run: ../../src/cli.js --lang python working-directory: examples/python - name: Check whether files were modified run: git status --short diff --git a/src/cli.js b/src/cli.js index e00ca06..0b80770 100755 --- a/src/cli.js +++ b/src/cli.js @@ -16,12 +16,9 @@ const parseCommandLineArgs = (args) => { const opts = { // @todo #24 Document --dest-dir option 'string': [ 'lang', 'dest-dir' ], - // @todo #33 Document --overwrite option - 'boolean': [ 'overwrite' ], 'default': { 'lang': 'js', - 'dest-dir': '.', - 'overwrite': false, + 'dest-dir': '.' } } const argv = parseArgs(args, opts) @@ -90,30 +87,7 @@ const findFileNamesEndWith = (dir, postfix) => { return fs.readdirSync(dir).filter(name => name.endsWith(postfix)) } -const stripPrefix = (text, prefix) => { - if (text.startsWith(prefix)) { - return text.slice(prefix.length) - } - return text -} - -const fileExistsHandler = (err) => { - if (err === null) { - // Success - return - } - //console.log(err) - if (err.code === 'EEXIST') { - // copyFile() puts original file name in err.path, so we use err.dest in that case - const filePath = err.dest || err.path - const file = stripPrefix(filePath, process.cwd() + '/') - console.warn(`WARNING: File ${file} already exists and won't be rewritten to preserve possible modifications. In order to overwrite the file, re-run the application with --overwrite option`) - return - } - throw err -} - -const createApp = async (destDir, { lang, overwrite }) => { +const createApp = async (destDir, { lang }) => { const ext = lang2extension(lang) const fileName = `app.${ext}` console.log('Generate', fileName) @@ -132,11 +106,10 @@ const createApp = async (destDir, { lang, overwrite }) => { } ) - const fsFlags = overwrite ? 'w' : 'wx' - return fsPromises.writeFile(resultFile, resultedCode, { 'flag': fsFlags }).catch(fileExistsHandler) + return fsPromises.writeFile(resultFile, resultedCode) } -const createDb = async (destDir, { lang, overwrite }) => { +const createDb = async (destDir, { lang }) => { if (lang !== 'python') { return } @@ -144,8 +117,7 @@ const createDb = async (destDir, { lang, overwrite }) => { console.log('Generate', fileName) const resultFile = path.join(destDir, fileName) - const mode = overwrite ? 0 : fsPromises.constants.COPYFILE_EXCL - return fsPromises.copyFile(`${__dirname}/templates/${fileName}`, resultFile, mode).catch(fileExistsHandler) + return fsPromises.copyFile(`${__dirname}/templates/${fileName}`, resultFile) } // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" @@ -182,7 +154,7 @@ const lengthOfLongestString = (arr) => arr 0 /* initial value */ ) -const createEndpoints = async (destDir, { lang, overwrite }, config) => { +const createEndpoints = async (destDir, { lang }, config) => { const ext = lang2extension(lang) const fileName = `routes.${ext}` console.log('Generate', fileName) @@ -306,11 +278,10 @@ const createEndpoints = async (destDir, { lang, overwrite }, config) => { } ) - const fsFlags = overwrite ? 'w' : 'wx' - return fsPromises.writeFile(resultFile, resultedCode, { 'flag': fsFlags }).catch(fileExistsHandler) + return fsPromises.writeFile(resultFile, resultedCode) } -const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { +const createDependenciesDescriptor = async (destDir, { lang }) => { let fileName if (lang === 'js') { fileName = 'package.json' @@ -342,8 +313,7 @@ const createDependenciesDescriptor = async (destDir, { lang, overwrite }) => { } ) - const fsFlags = overwrite ? 'w' : 'wx' - return fsPromises.writeFile(resultFile, minimalPackageJson, { 'flag': fsFlags }).catch(fileExistsHandler) + return fsPromises.writeFile(resultFile, minimalPackageJson) } const showInstructions = (lang) => { From 624ba06000b3eef607da7e9f7b249e43a3c07c60 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 13 Nov 2022 15:30:51 +0700 Subject: [PATCH 124/346] refactor: rename import alias Relate to #27 --- src/templates/app.py.ejs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/templates/app.py.ejs b/src/templates/app.py.ejs index 466996e..d379863 100644 --- a/src/templates/app.py.ejs +++ b/src/templates/app.py.ejs @@ -1,8 +1,8 @@ <% -// "custom_routes.py" => "custom_route" +// "custom_routes.py" => "custom_router" function fileName2routerName(filename) { - return filename.replace(/_routes\.py$/, '_route') + return filename.replace(/_routes\.py$/, '_router') } // "custom_routes.py" => "custom_routes" @@ -22,4 +22,4 @@ app = FastAPI() app.include_router(router) <% customRouteFilenames.forEach(filename => { %> app.include_router(<%= fileName2routerName(filename) %>) -<% }) -%> \ No newline at end of file +<% }) -%> From df22aa2e76bdbb2e3b3103e0865026c304a417f8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 13 Nov 2022 16:05:09 +0700 Subject: [PATCH 125/346] ci: fail a pipeline when non-committed files have been found Part of #13 --- .github/workflows/generate-go-app.yml | 10 ++++++++-- .github/workflows/generate-js-app.yml | 10 ++++++++-- .github/workflows/generate-python-app.yml | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 1401492..c8b3b16 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -23,5 +23,11 @@ jobs: - name: Generate Golang + Chi application run: ../../src/cli.js --lang go working-directory: examples/go - - name: Check whether files were modified - run: git status --short + - name: Check whether all modified files have been committed + run: >- + MODIFIED_FILES="$(git status --short)" + if [ -n "$MODIFIED_FILES" ]; then + echo >&2 "ERROR: the following generated files have not been committed:" + echo >&2 "$MODIFIED_FILES" + exit 1 + fi diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 1a95c0d..45a9a1f 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -23,5 +23,11 @@ jobs: - name: Generate JavaScript + Express application run: ../../src/cli.js --lang js working-directory: examples/js - - name: Check whether files were modified - run: git status --short + - name: Check whether all modified files have been committed + run: >- + MODIFIED_FILES="$(git status --short)" + if [ -n "$MODIFIED_FILES" ]; then + echo >&2 "ERROR: the following generated files have not been committed:" + echo >&2 "$MODIFIED_FILES" + exit 1 + fi diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 71ed644..99ecf6a 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -23,5 +23,11 @@ jobs: - name: Generate Python + FastAPI application run: ../../src/cli.js --lang python working-directory: examples/python - - name: Check whether files were modified - run: git status --short + - name: Check whether all modified files have been committed + run: >- + MODIFIED_FILES="$(git status --short)" + if [ -n "$MODIFIED_FILES" ]; then + echo >&2 "ERROR: the following generated files have not been committed:" + echo >&2 "$MODIFIED_FILES" + exit 1 + fi From 52fd844e9371852d5088a74e13670f3b300ad2cf Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 13 Nov 2022 16:07:47 +0700 Subject: [PATCH 126/346] ci: add semicolons to make shell commands work with >- YAML notation Correction for df22aa2e76bdbb2e3b3103e0865026c304a417f8 commit. Part of #13 --- .github/workflows/generate-go-app.yml | 8 ++++---- .github/workflows/generate-js-app.yml | 8 ++++---- .github/workflows/generate-python-app.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index c8b3b16..3a7d085 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -25,9 +25,9 @@ jobs: working-directory: examples/go - name: Check whether all modified files have been committed run: >- - MODIFIED_FILES="$(git status --short)" + MODIFIED_FILES="$(git status --short)"; if [ -n "$MODIFIED_FILES" ]; then - echo >&2 "ERROR: the following generated files have not been committed:" - echo >&2 "$MODIFIED_FILES" - exit 1 + echo >&2 "ERROR: the following generated files have not been committed:"; + echo >&2 "$MODIFIED_FILES"; + exit 1; fi diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 45a9a1f..ff00cb7 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -25,9 +25,9 @@ jobs: working-directory: examples/js - name: Check whether all modified files have been committed run: >- - MODIFIED_FILES="$(git status --short)" + MODIFIED_FILES="$(git status --short)"; if [ -n "$MODIFIED_FILES" ]; then - echo >&2 "ERROR: the following generated files have not been committed:" - echo >&2 "$MODIFIED_FILES" - exit 1 + echo >&2 "ERROR: the following generated files have not been committed:"; + echo >&2 "$MODIFIED_FILES"; + exit 1; fi diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 99ecf6a..a03b4dc 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -25,9 +25,9 @@ jobs: working-directory: examples/python - name: Check whether all modified files have been committed run: >- - MODIFIED_FILES="$(git status --short)" + MODIFIED_FILES="$(git status --short)"; if [ -n "$MODIFIED_FILES" ]; then - echo >&2 "ERROR: the following generated files have not been committed:" - echo >&2 "$MODIFIED_FILES" - exit 1 + echo >&2 "ERROR: the following generated files have not been committed:"; + echo >&2 "$MODIFIED_FILES"; + exit 1; fi From 5d379d25518872986b70a0308650568bc3dd6565 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 13 Nov 2022 16:11:31 +0700 Subject: [PATCH 127/346] chore: commit forgotten auto-generated files Correction for 624ba06000b3eef607da7e9f7b249e43a3c07c60 commit. Relate to #27 --- examples/python/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/python/app.py b/examples/python/app.py index 4ad6909..773f1c9 100644 --- a/examples/python/app.py +++ b/examples/python/app.py @@ -1,10 +1,10 @@ from fastapi import FastAPI from routes import router -from custom_routes import router as custom_route +from custom_routes import router as custom_router app = FastAPI() app.include_router(router) -app.include_router(custom_route) +app.include_router(custom_router) From beee88b7e10afaa5d0299b1a0b16345ca837c1f4 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 5 Jul 2023 10:06:59 +0700 Subject: [PATCH 128/346] chore(js): use express built-in middleware for parsing JSON instead of bodyParser See for details: - https://stackoverflow.com/questions/47232187/express-json-vs-bodyparser-json - https://expressjs.com/en/api.html --- examples/js/app.js | 3 +-- examples/js/package.json | 1 - src/templates/app.js.ejs | 3 +-- src/templates/package.json.ejs | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/js/app.js b/examples/js/app.js index 4b92f0c..a74b51c 100644 --- a/examples/js/app.js +++ b/examples/js/app.js @@ -1,10 +1,9 @@ -const bodyParser = require('body-parser') const express = require('express') const mysql = require('mysql') const routes = require('./routes') const app = express() -app.use(bodyParser.json()) +app.use(express.json()) const pool = mysql.createPool({ connectionLimit: 2, diff --git a/examples/js/package.json b/examples/js/package.json index 6dd060b..1ba1d79 100644 --- a/examples/js/package.json +++ b/examples/js/package.json @@ -5,7 +5,6 @@ "start": "node app.js" }, "dependencies": { - "body-parser": "~1.19.0", "express": "~4.17.1", "mysql": "~2.18.1" } diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 4b92f0c..a74b51c 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -1,10 +1,9 @@ -const bodyParser = require('body-parser') const express = require('express') const mysql = require('mysql') const routes = require('./routes') const app = express() -app.use(bodyParser.json()) +app.use(express.json()) const pool = mysql.createPool({ connectionLimit: 2, diff --git a/src/templates/package.json.ejs b/src/templates/package.json.ejs index 555a000..109e68a 100644 --- a/src/templates/package.json.ejs +++ b/src/templates/package.json.ejs @@ -5,7 +5,6 @@ "start": "node app.js" }, "dependencies": { - "body-parser": "~1.19.0", "express": "~4.17.1", "mysql": "~2.18.1" } From 99c0bb5e3673458122d6376fb9904b0f1ac9bba0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 5 Jul 2023 10:20:30 +0700 Subject: [PATCH 129/346] build: update minimist to 1.2.8 Changelogs: - https://github.com/minimistjs/minimist/blob/v1.2.8/CHANGELOG.md - https://github.com/advisories/GHSA-xvch-5gv4-984h This also obsoletes #14 --- package-lock.json | 17 ++++++++++------- package.json | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c70c2fd..9c46091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "ejs": "~3.1.3", "js-yaml": "~3.14.0", - "minimist": "~1.2.5", + "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" }, "bin": { @@ -187,9 +187,12 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/node-sql-parser": { "version": "3.0.4", @@ -348,9 +351,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "node-sql-parser": { "version": "3.0.4", diff --git a/package.json b/package.json index 2e8421c..3af81a4 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "dependencies": { "ejs": "~3.1.3", "js-yaml": "~3.14.0", - "minimist": "~1.2.5", + "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" } } From 8aa0c0cc4be1e8628d57b88c7b4ca9d5ecff8a4e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 5 Jul 2023 10:35:43 +0700 Subject: [PATCH 130/346] build: update ejs to 3.1.9 This also obsoletes #18 --- package-lock.json | 266 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 149 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c46091..97181fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.2", "license": "GPL-2.0", "dependencies": { - "ejs": "~3.1.3", + "ejs": "~3.1.9", "js-yaml": "~3.14.0", "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" @@ -19,14 +19,17 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { @@ -38,14 +41,14 @@ } }, "node_modules/async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/big-integer": { "version": "1.6.48", @@ -65,43 +68,47 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/ejs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", - "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", - "hasInstallScript": true, + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "dependencies": { - "jake": "^10.6.1" + "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" @@ -110,14 +117,6 @@ "node": ">=0.10.0" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -131,36 +130,55 @@ } }, "node_modules/filelist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", - "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "minimatch": "^3.0.4" + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", "dependencies": { - "async": "0.9.x", - "chalk": "^2.4.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/js-yaml": { @@ -176,9 +194,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -211,24 +229,24 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } } }, "dependencies": { "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "argparse": { @@ -240,14 +258,14 @@ } }, "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "big-integer": { "version": "1.6.48", @@ -264,73 +282,85 @@ } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "ejs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", - "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "requires": { - "jake": "^10.6.1" + "jake": "^10.8.5" } }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "filelist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", - "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "requires": { - "minimatch": "^3.0.4" + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", "requires": { - "async": "0.9.x", - "chalk": "^2.4.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" } }, "js-yaml": { @@ -343,9 +373,9 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } @@ -369,11 +399,11 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } diff --git a/package.json b/package.json index 3af81a4..8232178 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "ejs": "~3.1.3", + "ejs": "~3.1.9", "js-yaml": "~3.14.0", "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" From 6dfd9341e77ef79ec30ecac08dbf289353ae4ba5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 09:28:26 +0700 Subject: [PATCH 131/346] fix(js): remove semicolons from generated app.js --- examples/js/app.js | 10 +++++----- src/templates/app.js.ejs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/js/app.js b/examples/js/app.js index a74b51c..5a4492f 100644 --- a/examples/js/app.js +++ b/examples/js/app.js @@ -14,20 +14,20 @@ const pool = mysql.createPool({ // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) queryFormat: function(query, values) { if (!values) { - return query; + return query } return query.replace(/\:(\w+)/g, function(txt, key) { if (values.hasOwnProperty(key)) { - return this.escape(values[key]); + return this.escape(values[key]) } - return txt; - }.bind(this)); + return txt + }.bind(this)) } }) routes.register(app, pool) -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) }) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index a74b51c..5a4492f 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -14,20 +14,20 @@ const pool = mysql.createPool({ // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) queryFormat: function(query, values) { if (!values) { - return query; + return query } return query.replace(/\:(\w+)/g, function(txt, key) { if (values.hasOwnProperty(key)) { - return this.escape(values[key]); + return this.escape(values[key]) } - return txt; - }.bind(this)); + return txt + }.bind(this)) } }) routes.register(app, pool) -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) }) From b692d7de1802d1aa9fa0ab00359205ee997345c0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 09:30:55 +0700 Subject: [PATCH 132/346] chore: add helpers for generating examples Usage: $ npm run gen-js-example $ npm run gen-go-example $ npm run gen-py-example --- .github/workflows/generate-go-app.yml | 3 +-- .github/workflows/generate-js-app.yml | 3 +-- .github/workflows/generate-python-app.yml | 3 +-- package.json | 5 ++++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 3a7d085..7b5e62e 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -21,8 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Golang + Chi application - run: ../../src/cli.js --lang go - working-directory: examples/go + run: npm run generate-go-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index ff00cb7..976e643 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -21,8 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate JavaScript + Express application - run: ../../src/cli.js --lang js - working-directory: examples/js + run: npm run generate-js-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index a03b4dc..2b1bea5 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,8 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: ../../src/cli.js --lang python - working-directory: examples/python + run: npm run generate-py-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/package.json b/package.json index 8232178..8ebdeae 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,10 @@ ], "scripts": { "start": "node src/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "gen-js-example": "cd examples/js; ../../src/cli.js --lang js", + "gen-go-example": "cd examples/go; ../../src/cli.js --lang go", + "gen-py-example": "cd examples/python; ../../src/cli.js --lang python" }, "dependencies": { "ejs": "~3.1.9", From 9ffd4ca48387ea02d6d4e391561ae64ed5c9d086 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 09:41:05 +0700 Subject: [PATCH 133/346] fix(js): correct indentation of generated routes.js --- examples/js/routes.js | 178 ++++++++++++++++++------------------ src/templates/routes.js.ejs | 96 ++++++++++--------- 2 files changed, 135 insertions(+), 139 deletions(-) diff --git a/examples/js/routes.js b/examples/js/routes.js index c3bfe20..125cd53 100644 --- a/examples/js/routes.js +++ b/examples/js/routes.js @@ -1,108 +1,106 @@ const register = (app, pool) => { - -app.get('/v1/categories/count', (req, res) => { - pool.query( - 'SELECT COUNT(*) AS counter FROM categories', - (err, rows, fields) => { - if (err) { - throw err - } - if (rows.length === 0) { - res.status(404).end() - return + app.get('/v1/categories/count', (req, res) => { + pool.query( + 'SELECT COUNT(*) AS counter FROM categories', + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) } - res.json(rows[0]) - } - ) -}) + ) + }) -app.get('/v1/collections/:collectionId/categories/count', (req, res) => { - pool.query( - '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', - { "collectionId": req.params.collectionId }, - (err, rows, fields) => { - if (err) { - throw err - } - if (rows.length === 0) { - res.status(404).end() - return + app.get('/v1/collections/:collectionId/categories/count', (req, res) => { + pool.query( + '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', + { "collectionId": req.params.collectionId }, + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) } - res.json(rows[0]) - } - ) -}) + ) + }) -app.get('/v1/categories', (req, res) => { - pool.query( - 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', - { "limit": req.query.limit }, - (err, rows, fields) => { - if (err) { - throw err + app.get('/v1/categories', (req, res) => { + pool.query( + 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', + { "limit": req.query.limit }, + (err, rows, fields) => { + if (err) { + throw err + } + res.json(rows) } - res.json(rows) - } - ) -}) + ) + }) -app.post('/v1/categories', (req, res) => { - pool.query( - 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, - (err, rows, fields) => { - if (err) { - throw err + app.post('/v1/categories', (req, res) => { + pool.query( + 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) + ) + }) -app.get('/v1/categories/:categoryId', (req, res) => { - pool.query( - 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', - { "categoryId": req.params.categoryId }, - (err, rows, fields) => { - if (err) { - throw err + app.get('/v1/categories/:categoryId', (req, res) => { + pool.query( + 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', + { "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) } - if (rows.length === 0) { - res.status(404).end() - return - } - res.json(rows[0]) - } - ) -}) + ) + }) -app.put('/v1/categories/:categoryId', (req, res) => { - pool.query( - 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, - (err, rows, fields) => { - if (err) { - throw err + app.put('/v1/categories/:categoryId', (req, res) => { + pool.query( + 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) + ) + }) -app.delete('/v1/categories/:categoryId', (req, res) => { - pool.query( - 'DELETE FROM categories WHERE id = :categoryId', - { "categoryId": req.params.categoryId }, - (err, rows, fields) => { - if (err) { - throw err + app.delete('/v1/categories/:categoryId', (req, res) => { + pool.query( + 'DELETE FROM categories WHERE id = :categoryId', + { "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) - + ) + }) } diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index e8ca6ea..42358ba 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -1,5 +1,4 @@ const register = (app, pool) => { - <% endpoints.forEach(function(endpoint) { const path = endpoint.path; @@ -14,81 +13,80 @@ endpoints.forEach(function(endpoint) { const sql = formatQuery(method.query); const params = extractParamsFromQuery(method.query); const formattedParams = params.length > 0 - ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' + ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' : '' if (hasGetOne || hasGetMany) { %> -app.get('<%- path %>', (req, res) => { - pool.query( - '<%- sql %>',<%- formattedParams %> - (err, rows, fields) => { - if (err) { - throw err - } + app.get('<%- path %>', (req, res) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } <% if (hasGetMany) { -%> - res.json(rows) + res.json(rows) <% } else { -%> - if (rows.length === 0) { - res.status(404).end() - return - } - res.json(rows[0]) + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) <% } -%> - } - ) -}) + } + ) + }) <% } if (method.name === 'post') { %> -app.post('<%- path %>', (req, res) => { - pool.query( - '<%- sql %>',<%- formattedParams %> - (err, rows, fields) => { - if (err) { - throw err + app.post('<%- path %>', (req, res) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) + ) + }) <% } if (method.name === 'put') { %> -app.put('<%- path %>', (req, res) => { - pool.query( - '<%- sql %>',<%- formattedParams %> - (err, rows, fields) => { - if (err) { - throw err + app.put('<%- path %>', (req, res) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) + ) + }) <% } if (method.name === 'delete') { %> -app.delete('<%- path %>', (req, res) => { - pool.query( - '<%- sql %>',<%- formattedParams %> - (err, rows, fields) => { - if (err) { - throw err + app.delete('<%- path %>', (req, res) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) } - res.sendStatus(204) - } - ) -}) + ) + }) <% } }); }); %> - } exports.register = register; From 6b97e00d6f7845ae546019576602e107381ccf8f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 10:58:19 +0700 Subject: [PATCH 134/346] chore: correct goal names to fix build on CI Correction for b692d7de1802d1aa9fa0ab00359205ee997345c0 commit. --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 7b5e62e..170070d 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Golang + Chi application - run: npm run generate-go-example + run: npm run gen-go-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 976e643..5ed60f9 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate JavaScript + Express application - run: npm run generate-js-example + run: npm run gen-js-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 2b1bea5..5d040ed 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,7 +21,7 @@ jobs: - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: npm run generate-py-example + run: npm run gen-py-example - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; From 85cb4b61194a4b50d0218bed7de21b58011887f5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 11:01:24 +0700 Subject: [PATCH 135/346] ci: restrict permissions of the github actions --- .github/workflows/generate-go-app.yml | 6 ++++++ .github/workflows/generate-js-app.yml | 6 ++++++ .github/workflows/generate-python-app.yml | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 170070d..ac2f532 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -3,6 +3,12 @@ name: Generate Golang app on: push: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + # NOTE: actions/upload-artifact makes no use of permissions + # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 + contents: read # for "git clone" + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 5ed60f9..4964149 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -3,6 +3,12 @@ name: Generate JavaScript app on: push: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + # NOTE: actions/upload-artifact makes no use of permissions + # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 + contents: read # for "git clone" + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 5d040ed..863674a 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -3,6 +3,12 @@ name: Generate Python app on: push: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + # NOTE: actions/upload-artifact makes no use of permissions + # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 + contents: read # for "git clone" + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on From d6ebf55c96f89960992be72b74bf0b78cc31097b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 7 Jul 2023 11:01:41 +0700 Subject: [PATCH 136/346] ci: explicitly specify to use bash to enable fail-fast behavior --- .github/workflows/generate-go-app.yml | 7 +++++++ .github/workflows/generate-js-app.yml | 7 +++++++ .github/workflows/generate-python-app.yml | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index ac2f532..0c72ba4 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -9,6 +9,13 @@ permissions: # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 contents: read # for "git clone" +defaults: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun + run: + # Enable fail-fast behavior using set -eo pipefail + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 4964149..dff73ea 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -9,6 +9,13 @@ permissions: # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 contents: read # for "git clone" +defaults: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun + run: + # Enable fail-fast behavior using set -eo pipefail + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 863674a..a1630c0 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -9,6 +9,13 @@ permissions: # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 contents: read # for "git clone" +defaults: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun + run: + # Enable fail-fast behavior using set -eo pipefail + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + jobs: generate-app: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on From 9383d47aee82c33dc75720fe34de2b48497f4f4f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 4 Aug 2023 11:31:40 +0700 Subject: [PATCH 137/346] refactor: group Golang examples as // --- README.md | 2 +- examples/go/{ => chi/mysql}/app.go | 0 examples/go/chi/mysql/endpoints.yaml | 1 + examples/go/{ => chi/mysql}/go.mod | 0 examples/go/{ => chi/mysql}/routes.go | 0 examples/go/endpoints.yaml | 1 - package.json | 2 +- 7 files changed, 3 insertions(+), 3 deletions(-) rename examples/go/{ => chi/mysql}/app.go (100%) create mode 120000 examples/go/chi/mysql/endpoints.yaml rename examples/go/{ => chi/mysql}/go.mod (100%) rename examples/go/{ => chi/mysql}/routes.go (100%) delete mode 120000 examples/go/endpoints.yaml diff --git a/README.md b/README.md index 3c9067b..6692ada 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/app.go)
[`routes.go`](examples/go/routes.go)
[`go.mod`](examples/go/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`db.py`](examples/python/db.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application diff --git a/examples/go/app.go b/examples/go/chi/mysql/app.go similarity index 100% rename from examples/go/app.go rename to examples/go/chi/mysql/app.go diff --git a/examples/go/chi/mysql/endpoints.yaml b/examples/go/chi/mysql/endpoints.yaml new file mode 120000 index 0000000..a01efc7 --- /dev/null +++ b/examples/go/chi/mysql/endpoints.yaml @@ -0,0 +1 @@ +../../../js/endpoints.yaml \ No newline at end of file diff --git a/examples/go/go.mod b/examples/go/chi/mysql/go.mod similarity index 100% rename from examples/go/go.mod rename to examples/go/chi/mysql/go.mod diff --git a/examples/go/routes.go b/examples/go/chi/mysql/routes.go similarity index 100% rename from examples/go/routes.go rename to examples/go/chi/mysql/routes.go diff --git a/examples/go/endpoints.yaml b/examples/go/endpoints.yaml deleted file mode 120000 index ff2e3db..0000000 --- a/examples/go/endpoints.yaml +++ /dev/null @@ -1 +0,0 @@ -../js/endpoints.yaml \ No newline at end of file diff --git a/package.json b/package.json index 8ebdeae..ee3e284 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "gen-js-example": "cd examples/js; ../../src/cli.js --lang js", - "gen-go-example": "cd examples/go; ../../src/cli.js --lang go", + "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", "gen-py-example": "cd examples/python; ../../src/cli.js --lang python" }, "dependencies": { From 7327df5e882e554c8af1646a165c54ea9802d415 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 4 Aug 2023 11:38:43 +0700 Subject: [PATCH 138/346] refactor: group Python examples as // --- README.md | 2 +- examples/python/endpoints.yaml | 1 - examples/python/{ => fastapi/postgres}/app.py | 0 examples/python/{ => fastapi/postgres}/custom_routes.py | 0 examples/python/{ => fastapi/postgres}/db.py | 0 examples/python/fastapi/postgres/endpoints.yaml | 1 + examples/python/{ => fastapi/postgres}/requirements.txt | 0 examples/python/{ => fastapi/postgres}/routes.py | 0 package.json | 2 +- 9 files changed, 3 insertions(+), 3 deletions(-) delete mode 120000 examples/python/endpoints.yaml rename examples/python/{ => fastapi/postgres}/app.py (100%) rename examples/python/{ => fastapi/postgres}/custom_routes.py (100%) rename examples/python/{ => fastapi/postgres}/db.py (100%) create mode 120000 examples/python/fastapi/postgres/endpoints.yaml rename examples/python/{ => fastapi/postgres}/requirements.txt (100%) rename examples/python/{ => fastapi/postgres}/routes.py (100%) diff --git a/README.md b/README.md index 6692ada..74c5db8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/app.py)
[`db.py`](examples/python/db.py)
[`routes.py`](examples/python/routes.py)
[`requirements.txt`](examples/python/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application | Language | Commands to run the application | diff --git a/examples/python/endpoints.yaml b/examples/python/endpoints.yaml deleted file mode 120000 index ff2e3db..0000000 --- a/examples/python/endpoints.yaml +++ /dev/null @@ -1 +0,0 @@ -../js/endpoints.yaml \ No newline at end of file diff --git a/examples/python/app.py b/examples/python/fastapi/postgres/app.py similarity index 100% rename from examples/python/app.py rename to examples/python/fastapi/postgres/app.py diff --git a/examples/python/custom_routes.py b/examples/python/fastapi/postgres/custom_routes.py similarity index 100% rename from examples/python/custom_routes.py rename to examples/python/fastapi/postgres/custom_routes.py diff --git a/examples/python/db.py b/examples/python/fastapi/postgres/db.py similarity index 100% rename from examples/python/db.py rename to examples/python/fastapi/postgres/db.py diff --git a/examples/python/fastapi/postgres/endpoints.yaml b/examples/python/fastapi/postgres/endpoints.yaml new file mode 120000 index 0000000..a01efc7 --- /dev/null +++ b/examples/python/fastapi/postgres/endpoints.yaml @@ -0,0 +1 @@ +../../../js/endpoints.yaml \ No newline at end of file diff --git a/examples/python/requirements.txt b/examples/python/fastapi/postgres/requirements.txt similarity index 100% rename from examples/python/requirements.txt rename to examples/python/fastapi/postgres/requirements.txt diff --git a/examples/python/routes.py b/examples/python/fastapi/postgres/routes.py similarity index 100% rename from examples/python/routes.py rename to examples/python/fastapi/postgres/routes.py diff --git a/package.json b/package.json index ee3e284..c1153db 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "gen-js-example": "cd examples/js; ../../src/cli.js --lang js", "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", - "gen-py-example": "cd examples/python; ../../src/cli.js --lang python" + "gen-py-example": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" }, "dependencies": { "ejs": "~3.1.9", From 4d0e1545d80653d8feaf2ddf6a7813848c4e9cb6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 4 Aug 2023 11:47:54 +0700 Subject: [PATCH 139/346] refactor: group JavaScript examples as // Required for #35 --- README.md | 2 +- examples/go/chi/mysql/endpoints.yaml | 2 +- examples/js/{ => express/mysql}/app.js | 0 examples/js/{ => express/mysql}/endpoints.yaml | 0 examples/js/{ => express/mysql}/package.json | 2 +- examples/js/{ => express/mysql}/routes.js | 0 examples/python/fastapi/postgres/endpoints.yaml | 2 +- package.json | 2 +- src/cli.js | 1 + 9 files changed, 6 insertions(+), 5 deletions(-) rename examples/js/{ => express/mysql}/app.js (100%) rename examples/js/{ => express/mysql}/endpoints.yaml (100%) rename examples/js/{ => express/mysql}/package.json (88%) rename examples/js/{ => express/mysql}/routes.js (100%) diff --git a/README.md b/README.md index 74c5db8..cc68eb3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Generate code | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/app.js)
[`routes.js`](examples/js/routes.js)
[`package.json`](examples/js/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | diff --git a/examples/go/chi/mysql/endpoints.yaml b/examples/go/chi/mysql/endpoints.yaml index a01efc7..9e6d040 120000 --- a/examples/go/chi/mysql/endpoints.yaml +++ b/examples/go/chi/mysql/endpoints.yaml @@ -1 +1 @@ -../../../js/endpoints.yaml \ No newline at end of file +../../../js/express/mysql/endpoints.yaml \ No newline at end of file diff --git a/examples/js/app.js b/examples/js/express/mysql/app.js similarity index 100% rename from examples/js/app.js rename to examples/js/express/mysql/app.js diff --git a/examples/js/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml similarity index 100% rename from examples/js/endpoints.yaml rename to examples/js/express/mysql/endpoints.yaml diff --git a/examples/js/package.json b/examples/js/express/mysql/package.json similarity index 88% rename from examples/js/package.json rename to examples/js/express/mysql/package.json index 1ba1d79..0d0fdc8 100644 --- a/examples/js/package.json +++ b/examples/js/express/mysql/package.json @@ -1,5 +1,5 @@ { - "name": "js", + "name": "mysql", "version": "1.0.0", "scripts": { "start": "node app.js" diff --git a/examples/js/routes.js b/examples/js/express/mysql/routes.js similarity index 100% rename from examples/js/routes.js rename to examples/js/express/mysql/routes.js diff --git a/examples/python/fastapi/postgres/endpoints.yaml b/examples/python/fastapi/postgres/endpoints.yaml index a01efc7..9e6d040 120000 --- a/examples/python/fastapi/postgres/endpoints.yaml +++ b/examples/python/fastapi/postgres/endpoints.yaml @@ -1 +1 @@ -../../../js/endpoints.yaml \ No newline at end of file +../../../js/express/mysql/endpoints.yaml \ No newline at end of file diff --git a/package.json b/package.json index c1153db..8e54753 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "scripts": { "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1", - "gen-js-example": "cd examples/js; ../../src/cli.js --lang js", + "gen-js-example": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", "gen-py-example": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" }, diff --git a/src/cli.js b/src/cli.js index 0b80770..161c9e2 100755 --- a/src/cli.js +++ b/src/cli.js @@ -309,6 +309,7 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { `${__dirname}/templates/${fileName}.ejs`, { // project name is being used only for package.json + // @todo #35 [js] Let a user to specify project name projectName } ) From ab5da847c8efecb3bfb61b23768d99ed16ff573b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 4 Aug 2023 11:50:20 +0700 Subject: [PATCH 140/346] chore: correct README.md to remove dependency on body-parser Should be in beee88b7e10afaa5d0299b1a0b16345ca837c1f4 commit. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc68eb3..bea6304 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Generate code | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express), [`body-parser`](https://www.npmjs.com/package/body-parser)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | From cd71fb109d990df1a5fa32a8e3a659bd9ee49226 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 8 Aug 2023 11:07:57 +0700 Subject: [PATCH 141/346] feat: support generation of TypeScript + Express + MySQL applications Fix #35 --- .github/workflows/generate-ts-app.yml | 45 ++++++++++ README.md | 1 + examples/ts/express/mysql/app.ts | 34 +++++++ examples/ts/express/mysql/endpoints.yaml | 1 + examples/ts/express/mysql/package.json | 17 ++++ examples/ts/express/mysql/routes.ts | 110 +++++++++++++++++++++++ examples/ts/express/mysql/tsconfig.json | 109 ++++++++++++++++++++++ package.json | 1 + src/cli.js | 32 ++++++- src/templates/app.ts.ejs | 34 +++++++ src/templates/package.json.ejs | 13 +++ src/templates/routes.ts.ejs | 95 ++++++++++++++++++++ src/templates/tsconfig.json.ejs | 110 +++++++++++++++++++++++ 13 files changed, 600 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/generate-ts-app.yml create mode 100644 examples/ts/express/mysql/app.ts create mode 120000 examples/ts/express/mysql/endpoints.yaml create mode 100644 examples/ts/express/mysql/package.json create mode 100644 examples/ts/express/mysql/routes.ts create mode 100644 examples/ts/express/mysql/tsconfig.json create mode 100644 src/templates/app.ts.ejs create mode 100644 src/templates/routes.ts.ejs create mode 100644 src/templates/tsconfig.json.ejs diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml new file mode 100644 index 0000000..91b5583 --- /dev/null +++ b/.github/workflows/generate-ts-app.yml @@ -0,0 +1,45 @@ +name: Generate TypeScript app + +on: + push: + +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + # NOTE: actions/upload-artifact makes no use of permissions + # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 + contents: read # for "git clone" + +defaults: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun + run: + # Enable fail-fast behavior using set -eo pipefail + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +jobs: + generate-app: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-20.04 + steps: + - name: Clone source code + uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + with: + # Whether to configure the token or SSH key with the local git config. Default: true + persist-credentials: false + - name: Setup NodeJS + uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + with: + node-version: 18 + cache: 'npm' + - name: Install project dependencies + run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate TypeScript + Express application + run: npm run gen-ts-example + - name: Check whether all modified files have been committed + run: >- + MODIFIED_FILES="$(git status --short)"; + if [ -n "$MODIFIED_FILES" ]; then + echo >&2 "ERROR: the following generated files have not been committed:"; + echo >&2 "$MODIFIED_FILES"; + exit 1; + fi diff --git a/README.md b/README.md index bea6304..785b484 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.js)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts new file mode 100644 index 0000000..3393a75 --- /dev/null +++ b/examples/ts/express/mysql/app.ts @@ -0,0 +1,34 @@ +import express from 'express' +import mysql from 'mysql' + +const routes = require('./routes') + +const app = express() +app.use(express.json()) + +const pool = mysql.createPool({ + connectionLimit: 2, + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) + queryFormat: function(query, values) { + if (!values) { + return query + } + return query.replace(/\:(\w+)/g, function(txt, key) { + if (values.hasOwnProperty(key)) { + return this.escape(values[key]) + } + return txt + }.bind(this)) + } +}) + +routes.register(app, pool) + +const port = process.env.PORT || 3000 +app.listen(port, () => { + console.log(`Listen on ${port}`) +}) diff --git a/examples/ts/express/mysql/endpoints.yaml b/examples/ts/express/mysql/endpoints.yaml new file mode 120000 index 0000000..9e6d040 --- /dev/null +++ b/examples/ts/express/mysql/endpoints.yaml @@ -0,0 +1 @@ +../../../js/express/mysql/endpoints.yaml \ No newline at end of file diff --git a/examples/ts/express/mysql/package.json b/examples/ts/express/mysql/package.json new file mode 100644 index 0000000..e75c798 --- /dev/null +++ b/examples/ts/express/mysql/package.json @@ -0,0 +1,17 @@ +{ + "name": "mysql", + "version": "1.0.0", + "scripts": { + "build": "npx tsc", + "start": "node dist/app.js" + }, + "dependencies": { + "express": "~4.17.1", + "mysql": "~2.18.1" + }, + "devDependencies": { + "@types/express": "~4.17.17", + "@types/mysql": "~2.15.21", + "typescript": "~5.1.6" + } +} diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts new file mode 100644 index 0000000..f7e7e84 --- /dev/null +++ b/examples/ts/express/mysql/routes.ts @@ -0,0 +1,110 @@ +import { Express, Request, Response } from 'express' +import { Pool } from 'mysql' + +const register = (app: Express, pool: Pool) => { + + app.get('/v1/categories/count', (req: Request, res: Response) => { + pool.query( + 'SELECT COUNT(*) AS counter FROM categories', + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) + } + ) + }) + + app.get('/v1/collections/:collectionId/categories/count', (req: Request, res: Response) => { + pool.query( + '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', + { "collectionId": req.params.collectionId }, + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) + } + ) + }) + + app.get('/v1/categories', (req: Request, res: Response) => { + pool.query( + 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', + { "limit": req.query.limit }, + (err, rows, fields) => { + if (err) { + throw err + } + res.json(rows) + } + ) + }) + + app.post('/v1/categories', (req: Request, res: Response) => { + pool.query( + 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) + + app.get('/v1/categories/:categoryId', (req: Request, res: Response) => { + pool.query( + 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', + { "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) + } + ) + }) + + app.put('/v1/categories/:categoryId', (req: Request, res: Response) => { + pool.query( + 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) + + app.delete('/v1/categories/:categoryId', (req: Request, res: Response) => { + pool.query( + 'DELETE FROM categories WHERE id = :categoryId', + { "categoryId": req.params.categoryId }, + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) + +} + +exports.register = register; diff --git a/examples/ts/express/mysql/tsconfig.json b/examples/ts/express/mysql/tsconfig.json new file mode 100644 index 0000000..904a393 --- /dev/null +++ b/examples/ts/express/mysql/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/package.json b/package.json index 8e54753..fba602a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "start": "node src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "gen-js-example": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", + "gen-ts-example": "cd examples/ts/express/mysql; ../../../../src/cli.js --lang ts", "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", "gen-py-example": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" }, diff --git a/src/cli.js b/src/cli.js index 161c9e2..5a34436 100755 --- a/src/cli.js +++ b/src/cli.js @@ -74,6 +74,7 @@ const loadConfig = (endpointsFile) => { const lang2extension = (lang) => { switch (lang) { case 'js': + case 'ts': case 'go': return lang case 'python': @@ -283,7 +284,7 @@ const createEndpoints = async (destDir, { lang }, config) => { const createDependenciesDescriptor = async (destDir, { lang }) => { let fileName - if (lang === 'js') { + if (lang === 'js' || lang === 'ts') { fileName = 'package.json' } else if (lang === 'go') { @@ -301,13 +302,14 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { const resultFile = path.join(destDir, fileName) // @todo #24 [js] Possibly incorrect project name with --dest-dir option const projectName = path.basename(destDir) - if (lang === 'js') { + if (lang === 'js' || lang === 'ts') { console.log('Project name:', projectName) } const minimalPackageJson = await ejs.renderFile( `${__dirname}/templates/${fileName}.ejs`, { + lang, // project name is being used only for package.json // @todo #35 [js] Let a user to specify project name projectName @@ -317,6 +319,22 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { return fsPromises.writeFile(resultFile, minimalPackageJson) } +const createTypeScriptConfig = async (destDir, lang) => { + if (lang !== 'ts') { + return + } + const fileName = 'tsconfig.json' + console.log('Generate', fileName) + + const resultFile = path.join(destDir, fileName) + + const tsConfigJson = await ejs.renderFile( + `${__dirname}/templates/${fileName}.ejs` + ) + + return fsPromises.writeFile(resultFile, tsConfigJson) +} + const showInstructions = (lang) => { console.info('The application has been generated!') if (lang === 'js') { @@ -325,6 +343,15 @@ const showInstructions = (lang) => { to install its dependencies and export DB_NAME=db DB_USER=user DB_PASSWORD=secret npm start +afteward to run`) + } else if (lang === 'ts') { + console.info(`Use + npm install +to install its dependencies, + npm run build +to build the application, and + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + npm start afteward to run`) } else if (lang === 'go') { console.info(`Use @@ -365,6 +392,7 @@ const main = async (argv) => { await createDb(destDir, argv) await createEndpoints(destDir, argv, config) await createDependenciesDescriptor(destDir, argv) + await createTypeScriptConfig(destDir, argv.lang) showInstructions(argv.lang) } diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs new file mode 100644 index 0000000..3393a75 --- /dev/null +++ b/src/templates/app.ts.ejs @@ -0,0 +1,34 @@ +import express from 'express' +import mysql from 'mysql' + +const routes = require('./routes') + +const app = express() +app.use(express.json()) + +const pool = mysql.createPool({ + connectionLimit: 2, + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) + queryFormat: function(query, values) { + if (!values) { + return query + } + return query.replace(/\:(\w+)/g, function(txt, key) { + if (values.hasOwnProperty(key)) { + return this.escape(values[key]) + } + return txt + }.bind(this)) + } +}) + +routes.register(app, pool) + +const port = process.env.PORT || 3000 +app.listen(port, () => { + console.log(`Listen on ${port}`) +}) diff --git a/src/templates/package.json.ejs b/src/templates/package.json.ejs index 109e68a..523c7ea 100644 --- a/src/templates/package.json.ejs +++ b/src/templates/package.json.ejs @@ -2,10 +2,23 @@ "name": "<%- projectName %>", "version": "1.0.0", "scripts": { +<% if (lang === 'js') { -%> "start": "node app.js" +<% } else if (lang === 'ts') { -%> + "build": "npx tsc", + "start": "node dist/app.js" +<% } -%> }, "dependencies": { "express": "~4.17.1", "mysql": "~2.18.1" +<% if (lang === 'ts') { -%> + }, + "devDependencies": { +<%# Generated by: npm install --save-dev typescript @types/express @types/mysql -%> + "@types/express": "~4.17.17", + "@types/mysql": "~2.15.21", + "typescript": "~5.1.6" +<% } -%> } } diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs new file mode 100644 index 0000000..3ccd516 --- /dev/null +++ b/src/templates/routes.ts.ejs @@ -0,0 +1,95 @@ +import { Express, Request, Response } from 'express' +import { Pool } from 'mysql' + +const register = (app: Express, pool: Pool) => { +<% +endpoints.forEach(function(endpoint) { + const path = endpoint.path; + + endpoint.methods.forEach(function(method) { + if (!method.query) { + // filter out aggregated_queries for a while (see #17) + return + } + const hasGetOne = method.name === 'get'; + const hasGetMany = method.name === 'get_list'; + const sql = formatQuery(method.query); + const params = extractParamsFromQuery(method.query); + const formattedParams = params.length > 0 + ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' + : '' + + if (hasGetOne || hasGetMany) { +%> + app.get('<%- path %>', (req: Request, res: Response) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } +<% if (hasGetMany) { -%> + res.json(rows) +<% } else { -%> + if (rows.length === 0) { + res.status(404).end() + return + } + res.json(rows[0]) +<% } -%> + } + ) + }) +<% + } + if (method.name === 'post') { +%> + app.post('<%- path %>', (req: Request, res: Response) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) +<% + } + if (method.name === 'put') { +%> + app.put('<%- path %>', (req: Request, res: Response) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) +<% + } + if (method.name === 'delete') { +%> + app.delete('<%- path %>', (req: Request, res: Response) => { + pool.query( + '<%- sql %>',<%- formattedParams %> + (err, rows, fields) => { + if (err) { + throw err + } + res.sendStatus(204) + } + ) + }) +<% + } + }); +}); +%> +} + +exports.register = register; diff --git a/src/templates/tsconfig.json.ejs b/src/templates/tsconfig.json.ejs new file mode 100644 index 0000000..86be3a2 --- /dev/null +++ b/src/templates/tsconfig.json.ejs @@ -0,0 +1,110 @@ +<%# Generated by: npx tsc --init --outDir dist -%> +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From a3b9772023887ac73eda7f9fc1d5d3e5e63e5bf4 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Feb 2024 10:40:48 +0700 Subject: [PATCH 142/346] test: rewrite integration tests to use hurl Fix #40 --- .gitignore | 5 ---- tests/crud.hurl | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/crud.robot | 56 ----------------------------------- 3 files changed, 76 insertions(+), 61 deletions(-) create mode 100644 tests/crud.hurl delete mode 100644 tests/crud.robot diff --git a/.gitignore b/.gitignore index a6e3b0d..c2658d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ node_modules/ - -# RobotFramework reports -tests/log.html -tests/output.xml -tests/report.html diff --git a/tests/crud.hurl b/tests/crud.hurl new file mode 100644 index 0000000..7333368 --- /dev/null +++ b/tests/crud.hurl @@ -0,0 +1,76 @@ +# +# Tests for basic CRUD operations +# +# How to run: +# hurl --variable SERVER_URL=http://127.0.0.1:3000 crud.hurl --test +# +# See also: https://hurl.dev and https://github.com/Orange-OpenSource/hurl +# + + +# POST should create an object +POST {{ SERVER_URL }}/v1/categories +{ + "name": "Sport", + "slug": "sport", + "user_id": 1 +} +HTTP 204 + +# ensures that it was created +GET {{ SERVER_URL }}/v1/categories/count +HTTP 200 +[Asserts] +jsonpath "$.counter" == 1 + + +# GET should return a value +GET {{ SERVER_URL }}/v1/categories/1 +HTTP 200 +[Asserts] +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$.id" == 1 +jsonpath "$.name" == "Sport" +jsonpath "$.name_ru" == null +jsonpath "$.slug" == "sport" + + +# GET should return a list of values +GET {{ SERVER_URL }}/v1/categories +HTTP 200 +[Asserts] +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 1 +jsonpath "$[0].id" == 1 +jsonpath "$[0].name" == "Sport" +jsonpath "$[0].name_ru" == null +jsonpath "$[0].slug" == "sport" + + +# PUT should update an object +PUT {{ SERVER_URL }}/v1/categories/1 +{ + "name": "Fauna", + "name_ru": "Фауна", + "slug": "fauna", + "user_id": 1 +} +HTTP 204 + +# ensures that it was updated +GET {{ SERVER_URL }}/v1/categories/1 +HTTP 200 +[Asserts] +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$.name" == "Fauna" +jsonpath "$.name_ru" == "Фауна" +jsonpath "$.slug" == "fauna" + + +# DELETE should remove an object +DELETE {{ SERVER_URL }}/v1/categories/1 +HTTP 204 + +# ensures that it was removed +GET {{ SERVER_URL }}/v1/categories/1 +HTTP 404 diff --git a/tests/crud.robot b/tests/crud.robot deleted file mode 100644 index 90a58a0..0000000 --- a/tests/crud.robot +++ /dev/null @@ -1,56 +0,0 @@ -*** Settings *** -Documentation Basic CRUD operations -Library Collections -Library RequestsLibrary -Suite Setup Create Session alias=api url=${SERVER_URL} -Suite Teardown Delete All Sessions - -*** Variables *** -${SERVER_URL} http://host.docker.internal:3000 - -** Test Cases *** -POST should create an object - &{payload}= Create Dictionary name=Sport slug=sport user_id=1 - ${response}= Post Request api /v1/categories json=${payload} - Status Should Be 204 ${response} - # checks that it was created - ${response}= Get Request api /v1/categories/count - Status Should Be 200 ${response} - Dictionary Should Contain Item ${response.json()} counter 1 - -GET should return a value - ${response}= Get Request api /v1/categories/1 - ${body}= Set Variable ${response.json()} - Status Should Be 200 ${response} - Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 - &{expected}= Create Dictionary id=${1} name=Sport name_ru=${null} slug=sport - Dictionaries Should Be Equal ${body} ${expected} - -GET should return a list of values - ${response}= Get Request api /v1/categories - ${body}= Set Variable ${response.json()} - Status Should Be 200 ${response} - Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 - &{expected}= Create Dictionary id=${1} name=Sport name_ru=${null} slug=sport - Length Should Be ${body} 1 - Dictionaries Should Be Equal ${body[0]} ${expected} - -PUT should update an object - &{payload}= Create Dictionary name=Fauna name_ru=Фауна slug=fauna user_id=1 - ${response}= Put Request api /v1/categories/1 json=${payload} - Status Should Be 204 ${response} - # checks that it was updated - ${response}= Get Request api /v1/categories/1 - ${body}= Set Variable ${response.json()} - Status Should Be 200 ${response} - Should Be Equal ${response.headers['Content-Type']} application/json; charset=utf-8 - Dictionary Should Contain Item ${body} name Fauna - Dictionary Should Contain Item ${body} name_ru Фауна - Dictionary Should Contain Item ${body} slug fauna - -DELETE should remove an object - ${response}= Delete Request api /v1/categories/1 - Status Should Be 204 ${response} - # checks that it was removed - ${response}= Get Request api /v1/categories/1 - Status Should Be 404 ${response} From 79cf7459283a1f38ab73e1d99badf90d0f621599 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 20 Feb 2024 10:45:47 +0700 Subject: [PATCH 143/346] style: split steps by newlines --- .github/workflows/generate-go-app.yml | 5 +++++ .github/workflows/generate-js-app.yml | 5 +++++ .github/workflows/generate-python-app.yml | 5 +++++ .github/workflows/generate-ts-app.yml | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 0c72ba4..b89a916 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -21,20 +21,25 @@ jobs: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: + - name: Clone source code uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' + - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate Golang + Chi application run: npm run gen-go-example + - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index dff73ea..315f7f9 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -21,20 +21,25 @@ jobs: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: + - name: Clone source code uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' + - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate JavaScript + Express application run: npm run gen-js-example + - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index a1630c0..27fad16 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -21,20 +21,25 @@ jobs: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: + - name: Clone source code uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' + - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate Python + FastAPI application run: npm run gen-py-example + - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 91b5583..af42011 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -21,20 +21,25 @@ jobs: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: + - name: Clone source code uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false + - name: Setup NodeJS uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' + - name: Install project dependencies run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci + - name: Generate TypeScript + Express application run: npm run gen-ts-example + - name: Check whether all modified files have been committed run: >- MODIFIED_FILES="$(git status --short)"; From 848a7ad7c679047a511be3e53c8a1c4b3f5ed081 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 21 Feb 2024 11:07:03 +0700 Subject: [PATCH 144/346] style: correct SQL query indentation --- examples/js/express/mysql/endpoints.yaml | 4 ++-- examples/python/fastapi/postgres/routes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 4cd9448..14f6488 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -37,8 +37,8 @@ , name , name_ru , slug - FROM categories - LIMIT :q.limit + FROM categories + LIMIT :q.limit dto: name: CategoryDto fields: diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index ea9ae28..1bce328 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -75,8 +75,8 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)): , name , name_ru , slug - FROM categories - LIMIT %(limit)s + FROM categories + LIMIT %(limit)s """, {"limit": limit}) return cur.fetchall() finally: From 0d485ec0336aee72aecddc2d946c10fbc37ae6f1 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 9 Mar 2024 12:46:18 +0700 Subject: [PATCH 145/346] refactor: split logic for showing usage example into classes Part of #31 --- src/cli.js | 72 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/cli.js b/src/cli.js index 5a34436..0f69179 100755 --- a/src/cli.js +++ b/src/cli.js @@ -335,41 +335,79 @@ const createTypeScriptConfig = async (destDir, lang) => { return fsPromises.writeFile(resultFile, tsConfigJson) } -const showInstructions = (lang) => { - console.info('The application has been generated!') - if (lang === 'js') { - console.info(`Use +class JsGenerator { + + usageExampleAsText() { + return `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 === 'ts') { - console.info(`Use +afteward to run` + } + +} + +class TsGenerator { + + usageExampleAsText() { + return `Use npm install to install its dependencies, npm run build to build the application, and export DB_NAME=db DB_USER=user DB_PASSWORD=secret npm start -afteward to run`) - } else if (lang === 'go') { - console.info(`Use +afteward to run` + } + +} + +class GoGenerator { + + usageExampleAsText() { + return `Use export DB_NAME=db DB_USER=user DB_PASSWORD=secret go run *.go or go build -o app export DB_NAME=db DB_USER=user DB_PASSWORD=secret ./app -to build and run it`) - } else if (lang === 'python') { - console.info(`Use +to build and run it` + } + +} + +class PyGenerator { + + usageExampleAsText() { + return `Use pip install -r requirements.txt to install its dependencies and export DB_NAME=db DB_USER=user DB_PASSWORD=secret uvicorn app:app -afteward to run`) +afteward to run` + } + +} + +class Generator { + + static for(lang) { + switch (lang) { + case 'js': + return new JsGenerator() + case 'ts': + return new TsGenerator() + case 'go': + return new GoGenerator() + case 'python': + return new PyGenerator() + default: + throw new Error(`Unsupported language: ${lang}`) + } } + } const absolutePathToDestDir = (argv) => { @@ -388,12 +426,16 @@ const main = async (argv) => { fs.mkdirSync(destDir, {recursive: true}) } + const generator = Generator.for(argv.lang) + await createApp(destDir, argv) await createDb(destDir, argv) await createEndpoints(destDir, argv, config) await createDependenciesDescriptor(destDir, argv) await createTypeScriptConfig(destDir, argv.lang) - showInstructions(argv.lang) + + console.info('The application has been generated!') + console.info(generator.usageExampleAsText()) } From 4c4bcf26cc42ad4019710a9d166ba9d770d36007 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 9 Mar 2024 12:59:51 +0700 Subject: [PATCH 146/346] refactor: move classes into separate files Part of #31 --- src/cli.js | 77 +----------------------------------- src/generator/Generator.js | 23 +++++++++++ src/generator/GoGenerator.js | 14 +++++++ src/generator/JsGenerator.js | 12 ++++++ src/generator/PyGenerator.js | 12 ++++++ src/generator/TsGenerator.js | 14 +++++++ 6 files changed, 77 insertions(+), 75 deletions(-) create mode 100644 src/generator/Generator.js create mode 100644 src/generator/GoGenerator.js create mode 100644 src/generator/JsGenerator.js create mode 100644 src/generator/PyGenerator.js create mode 100644 src/generator/TsGenerator.js diff --git a/src/cli.js b/src/cli.js index 0f69179..fb87062 100755 --- a/src/cli.js +++ b/src/cli.js @@ -10,6 +10,8 @@ const parseArgs = require('minimist') const { Parser } = require('node-sql-parser') +const Generator = require('./generator/Generator') + const endpointsFile = 'endpoints.yaml' const parseCommandLineArgs = (args) => { @@ -335,81 +337,6 @@ const createTypeScriptConfig = async (destDir, lang) => { return fsPromises.writeFile(resultFile, tsConfigJson) } -class JsGenerator { - - usageExampleAsText() { - return `Use - npm install -to install its dependencies and - export DB_NAME=db DB_USER=user DB_PASSWORD=secret - npm start -afteward to run` - } - -} - -class TsGenerator { - - usageExampleAsText() { - return `Use - npm install -to install its dependencies, - npm run build -to build the application, and - export DB_NAME=db DB_USER=user DB_PASSWORD=secret - npm start -afteward to run` - } - -} - -class GoGenerator { - - usageExampleAsText() { - return `Use - export DB_NAME=db DB_USER=user DB_PASSWORD=secret - go run *.go -or - go build -o app - export DB_NAME=db DB_USER=user DB_PASSWORD=secret - ./app -to build and run it` - } - -} - -class PyGenerator { - - usageExampleAsText() { - return `Use - pip install -r requirements.txt -to install its dependencies and - export DB_NAME=db DB_USER=user DB_PASSWORD=secret - uvicorn app:app -afteward to run` - } - -} - -class Generator { - - static for(lang) { - switch (lang) { - case 'js': - return new JsGenerator() - case 'ts': - return new TsGenerator() - case 'go': - return new GoGenerator() - case 'python': - return new PyGenerator() - default: - throw new Error(`Unsupported language: ${lang}`) - } - } - -} - const absolutePathToDestDir = (argv) => { const relativeDestDir = argv._.length > 0 ? argv._[0] : argv['dest-dir'] return path.resolve(process.cwd(), relativeDestDir) diff --git a/src/generator/Generator.js b/src/generator/Generator.js new file mode 100644 index 0000000..436253c --- /dev/null +++ b/src/generator/Generator.js @@ -0,0 +1,23 @@ +const JsGenerator = require('./JsGenerator') +const TsGenerator = require('./TsGenerator') +const GoGenerator = require('./GoGenerator') +const PyGenerator = require('./PyGenerator') + +module.exports = class Generator { + + static for(lang) { + switch (lang) { + case 'js': + return new JsGenerator() + case 'ts': + return new TsGenerator() + case 'go': + return new GoGenerator() + case 'python': + return new PyGenerator() + default: + throw new Error(`Unsupported language: ${lang}`) + } + } + +} diff --git a/src/generator/GoGenerator.js b/src/generator/GoGenerator.js new file mode 100644 index 0000000..1d34590 --- /dev/null +++ b/src/generator/GoGenerator.js @@ -0,0 +1,14 @@ +module.exports = class GoGenerator { + + usageExampleAsText() { + return `Use + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + go run *.go +or + go build -o app + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + ./app +to build and run it` + } + +} diff --git a/src/generator/JsGenerator.js b/src/generator/JsGenerator.js new file mode 100644 index 0000000..13f658c --- /dev/null +++ b/src/generator/JsGenerator.js @@ -0,0 +1,12 @@ +module.exports = class JsGenerator { + + usageExampleAsText() { + return `Use + npm install +to install its dependencies and + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + npm start +afteward to run` + } + +} diff --git a/src/generator/PyGenerator.js b/src/generator/PyGenerator.js new file mode 100644 index 0000000..1d871e0 --- /dev/null +++ b/src/generator/PyGenerator.js @@ -0,0 +1,12 @@ +module.exports = class PyGenerator { + + usageExampleAsText() { + return `Use + pip install -r requirements.txt +to install its dependencies and + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + uvicorn app:app +afteward to run` + } + +} diff --git a/src/generator/TsGenerator.js b/src/generator/TsGenerator.js new file mode 100644 index 0000000..0fa46cc --- /dev/null +++ b/src/generator/TsGenerator.js @@ -0,0 +1,14 @@ +module.exports = class TsGenerator { + + usageExampleAsText() { + return `Use + npm install +to install its dependencies, + npm run build +to build the application, and + export DB_NAME=db DB_USER=user DB_PASSWORD=secret + npm start +afteward to run` + } + +} From b090ac2beab5b0bf5f42829ad37c46eaade9d5e8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 9 Mar 2024 13:15:37 +0700 Subject: [PATCH 147/346] refactor: pass a short lang to Generator.for() Relate to #31 --- src/cli.js | 3 ++- src/generator/Generator.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli.js b/src/cli.js index fb87062..a4eba52 100755 --- a/src/cli.js +++ b/src/cli.js @@ -353,7 +353,8 @@ const main = async (argv) => { fs.mkdirSync(destDir, {recursive: true}) } - const generator = Generator.for(argv.lang) + const lang = lang2extension(argv.lang) + const generator = Generator.for(lang) await createApp(destDir, argv) await createDb(destDir, argv) diff --git a/src/generator/Generator.js b/src/generator/Generator.js index 436253c..b889f57 100644 --- a/src/generator/Generator.js +++ b/src/generator/Generator.js @@ -13,7 +13,7 @@ module.exports = class Generator { return new TsGenerator() case 'go': return new GoGenerator() - case 'python': + case 'py': return new PyGenerator() default: throw new Error(`Unsupported language: ${lang}`) From bb991e8293654412b785f573254bd27e75aae029 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 14 Mar 2024 08:53:48 +0700 Subject: [PATCH 148/346] chore: add instructions on how to run TS app Should be in cd71fb109d990df1a5fa32a8e3a659bd9ee49226 commit. Part of #35 [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 785b484..27f4a97 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Commands to run the application | | -----------| --------------------------------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| + | TypeScript |
$ npm install
$ npm run build
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app
| From 70ab6ab03d0d934e269ee0a622cda4ade79b5653 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 14 Mar 2024 08:58:09 +0700 Subject: [PATCH 149/346] chore: simplify curl command by using --json parameter [skip ci] --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27f4a97..026708b 100644 --- a/README.md +++ b/README.md @@ -77,20 +77,24 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Test that it works ```console - $ curl -i -H 'Content-Type: application/json' -d '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories + $ curl -i --json '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:33 GMT Connection: keep-alive + $ curl http://localhost:3000/v1/categories [{"id":1,"name":"Sport","name_ru":"Спорт","slug":"sport"}] - $ curl -i -H 'Content-Type: application/json' -d '{"name":"Fauna","name_ru":"Фауна","slug":"fauna","user_id":101}' -X PUT http://localhost:3000/v1/categories/1 + + $ curl -i --json '{"name":"Fauna","name_ru":"Фауна","slug":"fauna","user_id":101}' -X PUT http://localhost:3000/v1/categories/1 HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:34 GMT Connection: keep-alive + $ curl http://localhost:3000/v1/categories/1 {"id":1,"name":"Fauna","name_ru":"Фауна","slug":"fauna"} + $ curl -i -X DELETE http://localhost:3000/v1/categories/1 HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" From 7db66f36f4eb87399f95da0500b0d5d8d5b4b1b0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 14 Mar 2024 20:51:30 +0700 Subject: [PATCH 150/346] chore: fix a broken link in README.md Correction for cd71fb109d990df1a5fa32a8e3a659bd9ee49226 commit. Relate to #35 [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 026708b..5e6d757 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.js)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | From 00a1f9d07f8ba393ad27da6b92ad3f45a4c7b796 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 14 Mar 2024 21:00:46 +0700 Subject: [PATCH 151/346] feat: generate Dockerfiles for Express and FastAPI apps Part of #42 --- README.md | 6 +++--- examples/js/express/mysql/Dockerfile | 8 ++++++++ examples/python/fastapi/postgres/Dockerfile | 6 ++++++ examples/ts/express/mysql/Dockerfile | 9 +++++++++ src/cli.js | 13 +++++++++++++ src/templates/Dockerfile.js | 8 ++++++++ src/templates/Dockerfile.py | 6 ++++++ src/templates/Dockerfile.ts | 9 +++++++++ 8 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 examples/js/express/mysql/Dockerfile create mode 100644 examples/python/fastapi/postgres/Dockerfile create mode 100644 examples/ts/express/mysql/Dockerfile create mode 100644 src/templates/Dockerfile.js create mode 100644 src/templates/Dockerfile.py create mode 100644 src/templates/Dockerfile.ts diff --git a/README.md b/README.md index 5e6d757..f640bb8 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,10 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Generate code | Language | Command for code generation | Example of generated files | Libraries | | -----------| ----------------------------| ---------------------------| --------- | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application | Language | Commands to run the application | diff --git a/examples/js/express/mysql/Dockerfile b/examples/js/express/mysql/Dockerfile new file mode 100644 index 0000000..efc57fe --- /dev/null +++ b/examples/js/express/mysql/Dockerfile @@ -0,0 +1,8 @@ +FROM node:18-bookworm +WORKDIR /opt/app +COPY package.json ./ +ENV NPM_CONFIG_UPDATE_NOTIFIER=false +RUN npm install --no-audit --no-fund +COPY *.js ./ +USER node +CMD [ "npm", "start" ] diff --git a/examples/python/fastapi/postgres/Dockerfile b/examples/python/fastapi/postgres/Dockerfile new file mode 100644 index 0000000..312f62b --- /dev/null +++ b/examples/python/fastapi/postgres/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.7-bookworm +WORKDIR /opt/app +COPY requirements.txt ./ +RUN pip install --no-cache-dir --upgrade -r requirements.txt +COPY *.py ./ +CMD [ "uvicorn", "app:app", "--host", "0.0.0.0" ] diff --git a/examples/ts/express/mysql/Dockerfile b/examples/ts/express/mysql/Dockerfile new file mode 100644 index 0000000..82b761d --- /dev/null +++ b/examples/ts/express/mysql/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-bookworm +WORKDIR /opt/app +COPY package.json ./ +ENV NPM_CONFIG_UPDATE_NOTIFIER=false +RUN npm install --no-audit --no-fund +COPY tsconfig.json *.ts ./ +RUN npm run build +USER node +CMD [ "npm", "start" ] diff --git a/src/cli.js b/src/cli.js index a4eba52..424352b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -321,6 +321,18 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { return fsPromises.writeFile(resultFile, minimalPackageJson) } +const createDockerfile = async (destDir, lang) => { + if (lang == 'go') { + return + } + const fileName = 'Dockerfile' + console.log('Generate', fileName) + + const resultFile = path.join(destDir, fileName) + + return fsPromises.copyFile(`${__dirname}/templates/${fileName}.${lang}`, resultFile) +} + const createTypeScriptConfig = async (destDir, lang) => { if (lang !== 'ts') { return @@ -361,6 +373,7 @@ const main = async (argv) => { await createEndpoints(destDir, argv, config) await createDependenciesDescriptor(destDir, argv) await createTypeScriptConfig(destDir, argv.lang) + await createDockerfile(destDir, lang) console.info('The application has been generated!') console.info(generator.usageExampleAsText()) diff --git a/src/templates/Dockerfile.js b/src/templates/Dockerfile.js new file mode 100644 index 0000000..efc57fe --- /dev/null +++ b/src/templates/Dockerfile.js @@ -0,0 +1,8 @@ +FROM node:18-bookworm +WORKDIR /opt/app +COPY package.json ./ +ENV NPM_CONFIG_UPDATE_NOTIFIER=false +RUN npm install --no-audit --no-fund +COPY *.js ./ +USER node +CMD [ "npm", "start" ] diff --git a/src/templates/Dockerfile.py b/src/templates/Dockerfile.py new file mode 100644 index 0000000..312f62b --- /dev/null +++ b/src/templates/Dockerfile.py @@ -0,0 +1,6 @@ +FROM python:3.7-bookworm +WORKDIR /opt/app +COPY requirements.txt ./ +RUN pip install --no-cache-dir --upgrade -r requirements.txt +COPY *.py ./ +CMD [ "uvicorn", "app:app", "--host", "0.0.0.0" ] diff --git a/src/templates/Dockerfile.ts b/src/templates/Dockerfile.ts new file mode 100644 index 0000000..82b761d --- /dev/null +++ b/src/templates/Dockerfile.ts @@ -0,0 +1,9 @@ +FROM node:18-bookworm +WORKDIR /opt/app +COPY package.json ./ +ENV NPM_CONFIG_UPDATE_NOTIFIER=false +RUN npm install --no-audit --no-fund +COPY tsconfig.json *.ts ./ +RUN npm run build +USER node +CMD [ "npm", "start" ] From 7132da6a3a02bcf3243ff4944128a4099fa985cc Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 15 Mar 2024 20:59:40 +0700 Subject: [PATCH 152/346] feat: generate Dockerfile for Golang app Part of #42 --- README.md | 2 +- examples/go/chi/mysql/Dockerfile | 11 +++++++++++ src/cli.js | 3 --- src/templates/Dockerfile.go | 11 +++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 examples/go/chi/mysql/Dockerfile create mode 100644 src/templates/Dockerfile.go diff --git a/README.md b/README.md index f640bb8..d5bad85 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| --------- | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application diff --git a/examples/go/chi/mysql/Dockerfile b/examples/go/chi/mysql/Dockerfile new file mode 100644 index 0000000..5d60014 --- /dev/null +++ b/examples/go/chi/mysql/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.14 AS builder +WORKDIR /opt +COPY go.mod ./ +RUN go mod download +COPY *.go ./ +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app + +FROM scratch +WORKDIR /opt/app +COPY --from=builder /opt/app . +CMD [ "/opt/app/app" ] diff --git a/src/cli.js b/src/cli.js index 424352b..33ebb16 100755 --- a/src/cli.js +++ b/src/cli.js @@ -322,9 +322,6 @@ const createDependenciesDescriptor = async (destDir, { lang }) => { } const createDockerfile = async (destDir, lang) => { - if (lang == 'go') { - return - } const fileName = 'Dockerfile' console.log('Generate', fileName) diff --git a/src/templates/Dockerfile.go b/src/templates/Dockerfile.go new file mode 100644 index 0000000..5d60014 --- /dev/null +++ b/src/templates/Dockerfile.go @@ -0,0 +1,11 @@ +FROM golang:1.14 AS builder +WORKDIR /opt +COPY go.mod ./ +RUN go mod download +COPY *.go ./ +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app + +FROM scratch +WORKDIR /opt/app +COPY --from=builder /opt/app . +CMD [ "/opt/app/app" ] From 1282644c103d56f6485314fd02318b64ea4f5b2c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 10:52:46 +0700 Subject: [PATCH 153/346] chore: add name to a job --- .github/workflows/generate-go-app.yml | 1 + .github/workflows/generate-js-app.yml | 1 + .github/workflows/generate-python-app.yml | 1 + .github/workflows/generate-ts-app.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index b89a916..0b257b7 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -18,6 +18,7 @@ defaults: jobs: generate-app: + name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 315f7f9..db4fd2c 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -18,6 +18,7 @@ defaults: jobs: generate-app: + name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 27fad16..2baca23 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -18,6 +18,7 @@ defaults: jobs: generate-app: + name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index af42011..00b00ab 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -18,6 +18,7 @@ defaults: jobs: generate-app: + name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 steps: From 206d310a4be076a8d9917056d1ada16518bdf9fb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 11:45:44 +0700 Subject: [PATCH 154/346] ci: run integration tests for Express/JavaScript/MySQL Part of #13 --- .github/workflows/integration-tests.yml | 88 +++++++++++++++++++++++++ docker/docker-compose.yaml | 13 ++++ 2 files changed, 101 insertions(+) create mode 100644 .github/workflows/integration-tests.yml diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..07b6d60 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,88 @@ +name: Integration Tests + +on: + push: + +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + # NOTE: actions/upload-artifact makes no use of permissions + # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 + contents: read # for "git clone" + +defaults: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defaultsrun + run: + # Enable fail-fast behavior using set -eo pipefail + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +jobs: + run-integration-tests: + name: Integration Tests + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-20.04 + steps: + + - name: Clone source code + uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + with: + # Whether to configure the token or SSH key with the local git config. Default: true + persist-credentials: false + + - name: Install Hurl + run: | + DEB=hurl_4.2.0_amd64.deb + curl --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB + dpkg --install $DEB + + - name: Show tools versions + run: | + hurl --version + docker compose version + docker version + + - name: Start containers + working-directory: docker + run: >- + docker compose up \ + --build \ + --detach \ + --wait \ + express-js + + - name: Show container statuses + working-directory: docker + run: docker compose ps + + - name: Run integration tests + run: >- + hurl \ + --error-format long \ + --report-html hurl-reports \ + --variable SERVER_URL=http://127.0.0.1:3010 \ + --test \ + tests/crud.hurl + + - name: Save container logs + if: failure() + working-directory: docker + run: >- + docker compose logs | tee containers-logs.txt + + - name: Stop containers + if: always() + working-directory: docker + run: >- + docker compose down \ + --volumes \ + --remove-orphans \ + --rmi all + + - name: Save report + if: failure() + uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact + with: + name: report-and-logs + path: | + containers-logs.txt + hurl-reports/ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 9c61da9..e51bb47 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -19,3 +19,16 @@ services: volumes: - ./categories.mysql.sql:/docker-entrypoint-initdb.d/categories.sql + + express-js: + build: ../examples/js/express/mysql + environment: + - DB_NAME=test + - DB_USER=test + - DB_PASSWORD=test + - DB_HOST=mysql # defaults to localhost + - PORT=3010 # defaults to 3000 + ports: + - '3010:3010' + depends_on: + - mysql From 67e33b362a18d7a4f12e43b17733af3e8a78f45f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 11:47:34 +0700 Subject: [PATCH 155/346] chore: add a comment with a link to dockerhub --- docker/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e51bb47..d2ff29f 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -7,6 +7,7 @@ version: '3' services: mysql: + # https://hub.docker.com/_/mysql image: mysql:5.7.20 user: mysql:mysql environment: From 5c5ef9da0e607cc764fc4202bf057f165e69dea8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 11:50:57 +0700 Subject: [PATCH 156/346] chore: disable progress meter for curl Relate to #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 07b6d60..d688fb2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,7 +32,7 @@ jobs: - name: Install Hurl run: | DEB=hurl_4.2.0_amd64.deb - curl --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB + curl --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB dpkg --install $DEB - name: Show tools versions From 71f09944e9d94b0470b34f7e5b7ad70b73d49518 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 11:52:24 +0700 Subject: [PATCH 157/346] chore: fix installation of hurl Error was: dpkg: error: requested operation requires superuser privilege Correction for 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d688fb2..1f3488a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -33,7 +33,7 @@ jobs: run: | DEB=hurl_4.2.0_amd64.deb curl --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB - dpkg --install $DEB + sudo dpkg --install $DEB - name: Show tools versions run: | From 18c2c9d2b57fa2626bedbc871164a3db9dcf1616 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 11:56:22 +0700 Subject: [PATCH 158/346] chore: fix installation of hurl because of invalid file content Error was: dpkg-deb: error: 'hurl_4.2.0_amd64.deb' is not a Debian format archive Correction for 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1f3488a..ae06c45 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,7 +32,7 @@ jobs: - name: Install Hurl run: | DEB=hurl_4.2.0_amd64.deb - curl --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB + curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB sudo dpkg --install $DEB - name: Show tools versions From 83707b08d34bdd2608f7aa85a9080afdcc70b544 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 12:12:45 +0700 Subject: [PATCH 159/346] chore: don't remove base images during purging Should be in 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ae06c45..2d8c326 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -76,7 +76,7 @@ jobs: docker compose down \ --volumes \ --remove-orphans \ - --rmi all + --rmi local - name: Save report if: failure() From e244cb3ea725325a6d50fe60525f07d5777b0612 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 12:14:37 +0700 Subject: [PATCH 160/346] chore: fix saving of container logs as artifact Correction for 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- .github/workflows/integration-tests.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 2d8c326..511983a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -66,8 +66,7 @@ jobs: - name: Save container logs if: failure() working-directory: docker - run: >- - docker compose logs | tee containers-logs.txt + run: docker compose logs | tee ../hurl-reports/containers-logs.txt - name: Stop containers if: always() @@ -83,6 +82,4 @@ jobs: uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact with: name: report-and-logs - path: | - containers-logs.txt - hurl-reports/ + path: hurl-reports/ From 7532ffba3fd788f05776e978e5e75ed7bdc3676f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 12:15:26 +0700 Subject: [PATCH 161/346] chore: include timestamps in container logs Should be in 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 511983a..5efcfe2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -66,7 +66,7 @@ jobs: - name: Save container logs if: failure() working-directory: docker - run: docker compose logs | tee ../hurl-reports/containers-logs.txt + run: docker compose logs --timestamps | tee ../hurl-reports/containers-logs.txt - name: Stop containers if: always() From d20dd42f77cc262f4b0daf1e821ad68bb86aef41 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 19 Mar 2024 21:53:13 +0700 Subject: [PATCH 162/346] ci: configure dependabot to monitor update for GitHub Actions Part of #38 [skip ci] --- .github/dependabot.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a9aab58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +# See for details: +# - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates +# - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#allow + allow: + - dependency-name: "actions/checkout" + - dependency-name: "actions/setup-node" + - dependency-name: "actions/upload-artifact" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#scheduleinterval + schedule: + interval: "daily" + time: "06:00" + timezone: "Asia/Novosibirsk" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#commit-message + commit-message: + prefix: "ci" + assignees: [ "php-coder" ] + reviewers: [ "php-coder" ] + labels: [ "kind/dependency-update", "area/ci" ] + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit + open-pull-requests-limit: 1 From 94805749a9a9a78ce390b484ab4ba62f2f677dad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:55:13 +0000 Subject: [PATCH 163/346] ci: bump actions/checkout from 3.1.0 to 4.1.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 4.1.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.1.0...v4.1.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 0b257b7..db3a7ae 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index db4fd2c..5597a63 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 2baca23..8d90863 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 00b00ab..b874b49 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5efcfe2..4021dc0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v3.1.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 097a70887b537ef4962735cf5dddac065ccb8fb3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 21 Mar 2024 21:58:40 +0700 Subject: [PATCH 164/346] chore: app now waits till MySQL is up and runnning Correction for 206d310a4be076a8d9917056d1ada16518bdf9fb commit. Part of #13 --- docker/docker-compose.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d2ff29f..caf0a75 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -19,6 +19,15 @@ services: - '3306:3306' volumes: - ./categories.mysql.sql:/docker-entrypoint-initdb.d/categories.sql + healthcheck: + # Specifying "MYSQL_PWD" variable suppresses "Warning: Using a password on the command line interface can be insecure" + # Attention: MYSQL_PWD is deprecated as of MySQL 8.0; expect it to be removed in a future version of MySQL + # Note: double dollar sign protects variables from docker compose interpolation + test: "MYSQL_PWD=$$MYSQL_PASSWORD mysql --user=$$MYSQL_USER --silent --execute 'SELECT \"OK\" AS result'" + interval: 1s + timeout: 5s + retries: 10 + start_period: 1s express-js: @@ -32,4 +41,5 @@ services: ports: - '3010:3010' depends_on: - - mysql + mysql: + condition: service_healthy From 162d81737d8ad849ecc183b7a7601de304185b32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:05:55 +0000 Subject: [PATCH 165/346] ci: bump actions/setup-node from 3.5.1 to 4.0.2 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.5.1 to 4.0.2. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3.5.1...v4.0.2) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index db3a7ae..31002e4 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 5597a63..89d66da 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 8d90863..acad99a 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index b874b49..8432959 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v3.5.1 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 84e5ca4814b1d2ce39538cc42f7ecf046c0e991d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:22:15 +0700 Subject: [PATCH 166/346] chore: make headers shorter [skip ci] --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d5bad85..135066d 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,16 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `q` (query), `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. 1. Generate code - | Language | Command for code generation | Example of generated files | Libraries | - | -----------| ----------------------------| ---------------------------| --------- | + | Language | Command | Generated files | Dependencies | + | -----------| ----------------------------| ---------------------------| ------------ | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application - | Language | Commands to run the application | - | -----------| --------------------------------| + | Language | Commands | + | -----------| ---------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | TypeScript |
$ npm install
$ npm run build
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| From 3b541a580cfa3ad981d90c4f4a98aaf1bdee0648 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:25:37 +0700 Subject: [PATCH 167/346] style: remove trailing spaces [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 135066d..450c74a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) ``` Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `q` (query), `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. -1. Generate code +1. Generate code | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | From dae786bbc30b0eae9f9c83779ffc0009495c6a5e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:35:28 +0700 Subject: [PATCH 168/346] chore: collapse parts of README.md file See https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections [skip ci] --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 450c74a..eb86acf 100644 --- a/README.md +++ b/README.md @@ -45,14 +45,19 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) Note that the queries use a little unusual named parameters: `:b.name`, `:p.categoryId`, etc The prefixes `q` (query), `b` (body) and `p` (path) are used here in order to bind to parameters from the appropriate sources. The prefixes are needed only during code generation and they will absent from the resulted code. 1. Generate code +
+ Example commands | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | +
1. Run the application +
+ Example commands | Language | Commands | | -----------| ---------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| @@ -74,8 +79,11 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) * `DB_HOST` a database host (defaults to `localhost`) --- +
1. Test that it works +
+ Examples for curl ```console $ curl -i --json '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories HTTP/1.1 204 No Content @@ -101,3 +109,4 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) Date: Wed, 15 Jul 2020 18:06:35 GMT Connection: keep-alive ``` +
From 44aff191cd5332bb01874860563cb08385a1ffcb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:38:11 +0700 Subject: [PATCH 169/346] chore: fix markup Correction for dae786bbc30b0eae9f9c83779ffc0009495c6a5e commit. [skip ci] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index eb86acf..7dffa74 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Generate code
Example commands + | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | @@ -58,6 +59,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Run the application
Example commands + | Language | Commands | | -----------| ---------| | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| @@ -84,6 +86,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) 1. Test that it works
Examples for curl + ```console $ curl -i --json '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories HTTP/1.1 204 No Content From 38f16f2d4f7e54a3a0b560fd300cb8463be2c1eb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:46:18 +0700 Subject: [PATCH 170/346] chore: use non-breaking spaces to avoid commands wrapping [skip ci] --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7dffa74..24e4018 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) |
1. Run the application From aa0d5e49e520f73e663757e906ebc61490845f38 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:47:57 +0700 Subject: [PATCH 171/346] shore: use more non-breaking spaces to avoid commands wrapping [skip ci] --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 24e4018..e8641f3 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) |
1. Run the application From f1bf5e2ceb2ff1f6811a74eb8d7d79d62673bb62 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:50:41 +0700 Subject: [PATCH 172/346] chore: manually break a line to avoid command wrapping [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8641f3..b550150 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | -----------| ----------------------------| ---------------------------| ------------ | | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql), [`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql),
[`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) |
From c9245def2b3c260f3fd843b37757f1c5998cf1d5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 18:56:21 +0700 Subject: [PATCH 173/346] chore: simplify dependencies section to avoid commands wrapping [skip ci] --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b550150..210f3a7 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Language | Command | Generated files | Dependencies | | -----------| ----------------------------| ---------------------------| ------------ | - | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | Web: [`express`](https://www.npmjs.com/package/express)
Database: [`mysql`](https://www.npmjs.com/package/mysql) | - | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | Web: [`go-chi/chi`](https://github.com/go-chi/chi)
Database: [`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql),
[`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | - | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | Web: [FastAPI](https://github.com/tiangolo/fastapi), [Uvicorn](https://www.uvicorn.org)
Database: [psycopg2](https://pypi.org/project/psycopg2/) | + | JavaScript | `npx query2app --lang js` | [`app.js`](examples/js/express/mysql/app.js)
[`routes.js`](examples/js/express/mysql/routes.js)
[`package.json`](examples/js/express/mysql/package.json)
[`Dockerfile`](examples/js/express/mysql/Dockerfile) | [`express`](https://www.npmjs.com/package/express)
[`mysql`](https://www.npmjs.com/package/mysql) | + | TypeScript | `npx query2app --lang ts` | [`app.ts`](examples/ts/express/mysql/app.ts)
[`routes.ts`](examples/ts/express/mysql/routes.ts)
[`package.json`](examples/ts/express/mysql/package.json)
[`tsconfig.json`](examples/ts/express/mysql/tsconfig.json)
[`Dockerfile`](examples/ts/express/mysql/Dockerfile) | [`express`](https://www.npmjs.com/package/express)
[`mysql`](https://www.npmjs.com/package/mysql) | + | Golang | `npx query2app --lang go` | [`app.go`](examples/go/chi/mysql/app.go)
[`routes.go`](examples/go/chi/mysql/routes.go)
[`go.mod`](examples/go/chi/mysql/go.mod)
[`Dockerfile`](examples/go/chi/mysql/Dockerfile) | [`go-chi/chi`](https://github.com/go-chi/chi)
[`go-sql-driver/mysql`](https://github.com/go-sql-driver/mysql)
[`jmoiron/sqlx`](https://github.com/jmoiron/sqlx) | + | Python | `npx query2app --lang python` | [`app.py`](examples/python/fastapi/postgres/app.py)
[`db.py`](examples/python/fastapi/postgres/db.py)
[`routes.py`](examples/python/fastapi/postgres/routes.py)
[`requirements.txt`](examples/python/fastapi/postgres/requirements.txt)
[`Dockerfile`](examples/python/fastapi/postgres/Dockerfile) | [FastAPI](https://github.com/tiangolo/fastapi)
[Uvicorn](https://www.uvicorn.org)
[psycopg2](https://pypi.org/project/psycopg2/) | 1. Run the application From 3bf753e1d9ca5cc5c83d796a8ff250e8fa3e04e9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 19:00:44 +0700 Subject: [PATCH 174/346] chore: split curl commans by multiple lines to avoid horizontal scroll [skip ci] --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 210f3a7..99a3481 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) Examples for curl ```console - $ curl -i --json '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' http://localhost:3000/v1/categories + $ curl -i http://localhost:3000/v1/categories \ + --json '{"name":"Sport","name_ru":"Спорт","slug":"sport","user_id":100}' HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:33 GMT @@ -97,7 +98,8 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) $ curl http://localhost:3000/v1/categories [{"id":1,"name":"Sport","name_ru":"Спорт","slug":"sport"}] - $ curl -i --json '{"name":"Fauna","name_ru":"Фауна","slug":"fauna","user_id":101}' -X PUT http://localhost:3000/v1/categories/1 + $ curl -i -X PUT http://localhost:3000/v1/categories/1 \ + --json '{"name":"Fauna","name_ru":"Фауна","slug":"fauna","user_id":101}' HTTP/1.1 204 No Content ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI" Date: Wed, 15 Jul 2020 18:06:34 GMT From 447d241ba892dd175bc266f25229e4fcaf295103 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 19:08:55 +0700 Subject: [PATCH 175/346] feat(js): handle internal server errors and avoid failing a whole app Part of #48 --- examples/js/express/mysql/routes.js | 32 ++++++++++++++++------------- src/templates/routes.js.ejs | 20 ++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 125cd53..0856eed 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -1,11 +1,11 @@ const register = (app, pool) => { - app.get('/v1/categories/count', (req, res) => { + app.get('/v1/categories/count', (req, res, next) => { pool.query( 'SELECT COUNT(*) AS counter FROM categories', (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -16,13 +16,13 @@ const register = (app, pool) => { ) }) - app.get('/v1/collections/:collectionId/categories/count', (req, res) => { + app.get('/v1/collections/:collectionId/categories/count', (req, res, next) => { pool.query( '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', { "collectionId": req.params.collectionId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -33,39 +33,39 @@ const register = (app, pool) => { ) }) - app.get('/v1/categories', (req, res) => { + app.get('/v1/categories', (req, res, next) => { pool.query( 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', { "limit": req.query.limit }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.json(rows) } ) }) - app.post('/v1/categories', (req, res) => { + app.post('/v1/categories', (req, res, next) => { pool.query( 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) - app.get('/v1/categories/:categoryId', (req, res) => { + app.get('/v1/categories/:categoryId', (req, res, next) => { pool.query( 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -76,32 +76,36 @@ const register = (app, pool) => { ) }) - app.put('/v1/categories/:categoryId', (req, res) => { + app.put('/v1/categories/:categoryId', (req, res, next) => { pool.query( 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) - app.delete('/v1/categories/:categoryId', (req, res) => { + app.delete('/v1/categories/:categoryId', (req, res, next) => { pool.query( 'DELETE FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) + app.use((error, req, res, next) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) + }) } exports.register = register; diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 42358ba..6c9fd47 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -18,12 +18,12 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> - app.get('<%- path %>', (req, res) => { + app.get('<%- path %>', (req, res, next) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } <% if (hasGetMany) { -%> res.json(rows) @@ -41,12 +41,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'post') { %> - app.post('<%- path %>', (req, res) => { + app.post('<%- path %>', (req, res, next) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -56,12 +56,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'put') { %> - app.put('<%- path %>', (req, res) => { + app.put('<%- path %>', (req, res, next) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -71,12 +71,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'delete') { %> - app.delete('<%- path %>', (req, res) => { + app.delete('<%- path %>', (req, res, next) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -87,6 +87,10 @@ endpoints.forEach(function(endpoint) { }); }); %> + app.use((error, req, res, next) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) + }) } exports.register = register; From 52df3bc4dff8fd990df2d90558d14cfedfeb3d1e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 19:22:51 +0700 Subject: [PATCH 176/346] refactor(go): rename variable nstmt to stmt --- examples/go/chi/mysql/routes.go | 12 ++++++------ src/templates/routes.go.ejs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index c9e38ef..0dbaaad 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -51,7 +51,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { - 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") + stmt, 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") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) w.WriteHeader(http.StatusInternalServerError) @@ -62,7 +62,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { args := map[string]interface{}{ "collectionId": chi.URLParam(r, "collectionId"), } - err = nstmt.Get(&result, args) + err = stmt.Get(&result, args) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) @@ -76,7 +76,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - nstmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories LIMIT :limit") + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories LIMIT :limit") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) w.WriteHeader(http.StatusInternalServerError) @@ -87,7 +87,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { args := map[string]interface{}{ "limit": r.URL.Query().Get("limit"), } - err = nstmt.Get(&result, args) + err = stmt.Get(&result, args) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) @@ -124,7 +124,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - nstmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId") + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) w.WriteHeader(http.StatusInternalServerError) @@ -135,7 +135,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { args := map[string]interface{}{ "categoryId": chi.URLParam(r, "categoryId"), } - err = nstmt.Get(&result, args) + err = stmt.Get(&result, args) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index f8d622d..2a3d2f5 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -184,7 +184,7 @@ endpoints.forEach(function(endpoint) { <% if (params.length > 0) { -%> - nstmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>") + stmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) w.WriteHeader(http.StatusInternalServerError) @@ -195,7 +195,7 @@ endpoints.forEach(function(endpoint) { args := map[string]interface{}{ <%- /* LATER: extract */ params.map(p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)](p.substring(2))},`).join('\n\t\t\t') %> } - err = nstmt.Get(&result, args) + err = stmt.Get(&result, args) <% } else { -%> var result <%- dataType %> err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>") From 6a5dc71b02b916c0c5d406a86f748af7cd36702c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 19:44:43 +0700 Subject: [PATCH 177/346] ci: run integration tests for Express/TypeScript/MySQL Part of #13 --- .github/workflows/integration-tests.yml | 15 +++++++++++++-- docker/docker-compose.yaml | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4021dc0..7a594d4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -21,6 +21,17 @@ jobs: name: Integration Tests # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: ubuntu-20.04 + # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + strategy: + matrix: + # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations + include: + # "docker-service-name" must match "services.$name" from docker-compose.yaml + # "application-port" must match "services.$name.environment:PORT" from docker-compose.yaml + - docker-service-name: 'express-js' + application-port: 3010 + - service-name: 'express-ts' + application-port: 3020 steps: - name: Clone source code @@ -48,7 +59,7 @@ jobs: --build \ --detach \ --wait \ - express-js + ${{ matrix.docker-service-name }} - name: Show container statuses working-directory: docker @@ -59,7 +70,7 @@ jobs: hurl \ --error-format long \ --report-html hurl-reports \ - --variable SERVER_URL=http://127.0.0.1:3010 \ + --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --test \ tests/crud.hurl diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index caf0a75..a433e7b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -43,3 +43,17 @@ services: depends_on: mysql: condition: service_healthy + + express-ts: + build: ../examples/ts/express/mysql + environment: + - DB_NAME=test + - DB_USER=test + - DB_PASSWORD=test + - DB_HOST=mysql # defaults to localhost + - PORT=3020 # defaults to 3000 + ports: + - '3020:3020' + depends_on: + mysql: + condition: service_healthy From cae21bd6bec0a3969345ec306f3c03d2cbe5229f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 20:00:04 +0700 Subject: [PATCH 178/346] chore: don't publish MySQL port to the host to avoid conflict on CI where 2 matrix jobs are running simultaneously Correction for 6a5dc71b02b916c0c5d406a86f748af7cd36702c commit. Relate to #13 --- docker/docker-compose.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index a433e7b..685e013 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -15,8 +15,6 @@ services: - MYSQL_USER=test - MYSQL_PASSWORD=test - MYSQL_DATABASE=test - ports: - - '3306:3306' volumes: - ./categories.mysql.sql:/docker-entrypoint-initdb.d/categories.sql healthcheck: From 07fdad589148b193bec4b39b0cb16248fe31d95c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 22:33:10 +0700 Subject: [PATCH 179/346] ci: run integration tests for Chi/Golang/MySQL Part of #13 --- .github/workflows/integration-tests.yml | 2 ++ docker/docker-compose.yaml | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7a594d4..a801e3d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,6 +32,8 @@ jobs: application-port: 3010 - service-name: 'express-ts' application-port: 3020 + - service-name: 'chi' + application-port: 3030 steps: - name: Clone source code diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 685e013..2bf4494 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -55,3 +55,17 @@ services: depends_on: mysql: condition: service_healthy + + chi: + build: ../examples/go/chi/mysql + environment: + - DB_NAME=test + - DB_USER=test + - DB_PASSWORD=test + - DB_HOST=mysql # defaults to localhost + - PORT=3030 # defaults to 3000 + ports: + - '3030:3030' + depends_on: + mysql: + condition: service_healthy From b6a200e52034d1438819787f9443125e6e7cdf52 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 22:38:51 +0700 Subject: [PATCH 180/346] chore: fix name of docker service for app on TypeScript Correction for 6a5dc71b02b916c0c5d406a86f748af7cd36702c commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a801e3d..d4e5c84 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -30,7 +30,7 @@ jobs: # "application-port" must match "services.$name.environment:PORT" from docker-compose.yaml - docker-service-name: 'express-js' application-port: 3010 - - service-name: 'express-ts' + - docker-service-name: 'express-ts' application-port: 3020 - service-name: 'chi' application-port: 3030 From 69a95551cb2e3b0036a269d0e1fceda36e216278 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 22:40:18 +0700 Subject: [PATCH 181/346] chore: fix name of docker service for app on Golang Correction for 07fdad589148b193bec4b39b0cb16248fe31d95c commit. Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d4e5c84..18ed0ff 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,7 +32,7 @@ jobs: application-port: 3010 - docker-service-name: 'express-ts' application-port: 3020 - - service-name: 'chi' + - docker-service-name: 'chi' application-port: 3030 steps: From 7385f2b557862a28b70a5ac16efe87e60b6da0cb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 22:56:53 +0700 Subject: [PATCH 182/346] chore: always show containers statuses in order to debug "The operation was canceled" error Relate to #13 --- .github/workflows/integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 18ed0ff..a13944d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -64,6 +64,7 @@ jobs: ${{ matrix.docker-service-name }} - name: Show container statuses + if: always() working-directory: docker run: docker compose ps From f647d6925522136ae9ef1ceb71ba7b166f579326 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 23 Mar 2024 23:30:10 +0700 Subject: [PATCH 183/346] chore: prevent interference between builds by setting the project name to a unique value Correction for 6a5dc71b02b916c0c5d406a86f748af7cd36702c commit. Part of #13 --- .github/workflows/integration-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a13944d..5a33b62 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -34,6 +34,12 @@ jobs: application-port: 3020 - docker-service-name: 'chi' application-port: 3030 + env: + # Prevent interference between builds by setting the project name to a unique value. Otherwise + # "docker compose down" has been stopping containers (especially database) from other builds. + # https://docs.docker.com/compose/project-name/ + # https://docs.docker.com/compose/environment-variables/envvars/#compose_project_name + COMPOSE_PROJECT_NAME: ${{ matrix.docker-service-name }} steps: - name: Clone source code From ff681aac6c7a6baa13739a0b40eaa59b8b7bf60f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 25 Mar 2024 10:03:14 +0700 Subject: [PATCH 184/346] style(js,ts): add a comment with a link to String.replace() API --- src/templates/app.js.ejs | 1 + src/templates/app.ts.ejs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 5a4492f..9daa981 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -16,6 +16,7 @@ const pool = mysql.createPool({ if (!values) { return query } +<%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> return query.replace(/\:(\w+)/g, function(txt, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 3393a75..39864be 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -17,6 +17,7 @@ const pool = mysql.createPool({ if (!values) { return query } +<%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> return query.replace(/\:(\w+)/g, function(txt, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) From 31927407f278203fc328364be6792c8c063a4e44 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 25 Mar 2024 10:05:50 +0700 Subject: [PATCH 185/346] refactor(js,ts): rename parameter txt to matchedSubstring --- examples/js/express/mysql/app.js | 4 ++-- examples/ts/express/mysql/app.ts | 4 ++-- src/templates/app.js.ejs | 4 ++-- src/templates/app.ts.ejs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index 5a4492f..e5e4e65 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -16,11 +16,11 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(txt, key) { + return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) } - return txt + return matchedSubstring }.bind(this)) } }) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index 3393a75..42af118 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -17,11 +17,11 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(txt, key) { + return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) } - return txt + return matchedSubstring }.bind(this)) } }) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 9daa981..afd11de 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -17,11 +17,11 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(txt, key) { + return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) } - return txt + return matchedSubstring }.bind(this)) } }) diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 39864be..c8a9a19 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -18,11 +18,11 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(txt, key) { + return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { if (values.hasOwnProperty(key)) { return this.escape(values[key]) } - return txt + return matchedSubstring }.bind(this)) } }) From aca4d44be85a80b297e0d9b4b4ed824750a549b8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 25 Mar 2024 10:08:07 +0700 Subject: [PATCH 186/346] refactor(js,ts): rename parameter key to capturedValue --- examples/js/express/mysql/app.js | 6 +++--- examples/ts/express/mysql/app.ts | 6 +++--- src/templates/app.js.ejs | 6 +++--- src/templates/app.ts.ejs | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index e5e4e65..84b6729 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -16,9 +16,9 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { - if (values.hasOwnProperty(key)) { - return this.escape(values[key]) + return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + if (values.hasOwnProperty(capturedValue)) { + return this.escape(values[capturedValue]) } return matchedSubstring }.bind(this)) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index 42af118..b11e0b6 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -17,9 +17,9 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { - if (values.hasOwnProperty(key)) { - return this.escape(values[key]) + return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + if (values.hasOwnProperty(capturedValue)) { + return this.escape(values[capturedValue]) } return matchedSubstring }.bind(this)) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index afd11de..24a4937 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -17,9 +17,9 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { - if (values.hasOwnProperty(key)) { - return this.escape(values[key]) + return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + if (values.hasOwnProperty(capturedValue)) { + return this.escape(values[capturedValue]) } return matchedSubstring }.bind(this)) diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index c8a9a19..0efd9d3 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -18,9 +18,9 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(matchedSubstring, key) { - if (values.hasOwnProperty(key)) { - return this.escape(values[key]) + return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + if (values.hasOwnProperty(capturedValue)) { + return this.escape(values[capturedValue]) } return matchedSubstring }.bind(this)) From 6995d81707c7571b909d1d644cc6b8671dd35b8a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 25 Mar 2024 10:09:35 +0700 Subject: [PATCH 187/346] chore(ts): fix error TS7006: Parameter 'matchedSubstring' implicitly has an 'any' type. Correction for cd71fb109d990df1a5fa32a8e3a659bd9ee49226 commit. Part of #35 --- examples/ts/express/mysql/app.ts | 2 +- src/templates/app.ts.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index b11e0b6..ae98048 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -17,7 +17,7 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 0efd9d3..623ffe5 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -18,7 +18,7 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { + return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } From 1fb04f3d768394887020df0b7d9fce3d5814b142 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 25 Mar 2024 10:09:35 +0700 Subject: [PATCH 188/346] chore(ts): fix error TS7006: Parameter 'capturedValue' implicitly has an 'any' type Correction for cd71fb109d990df1a5fa32a8e3a659bd9ee49226 commit. Part of #35 --- examples/ts/express/mysql/app.ts | 2 +- src/templates/app.ts.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index ae98048..23f314c 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -17,7 +17,7 @@ const pool = mysql.createPool({ if (!values) { return query } - return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue) { + return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue: string) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 623ffe5..5479510 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -18,7 +18,7 @@ const pool = mysql.createPool({ return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue) { + return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue: string) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } From 9a61040990d37c423d12fc028c2688dcd91d1b2a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 11:44:13 +0700 Subject: [PATCH 189/346] chore(ts): fix errors related to this keyword app.ts:22:24 - error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation. app.ts:20:42 An outer value of 'this' is shadowed by this container. Correction for cd71fb109d990df1a5fa32a8e3a659bd9ee49226 commit. Part of #35 --- examples/ts/express/mysql/app.ts | 4 ++-- src/templates/app.ts.ejs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index 23f314c..7cf4673 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -13,11 +13,11 @@ const pool = mysql.createPool({ password: process.env.DB_PASSWORD, database: process.env.DB_NAME, // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) - queryFormat: function(query, values) { + queryFormat: function(this: mysql.Pool, query, values) { if (!values) { return query } - return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue: string) { + return query.replace(/\:(\w+)/g, function(this: mysql.Pool, matchedSubstring: string, capturedValue: string) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 5479510..d079d84 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -13,12 +13,12 @@ const pool = mysql.createPool({ password: process.env.DB_PASSWORD, database: process.env.DB_NAME, // Support of named placeholders (https://github.com/mysqljs/mysql#custom-format) - queryFormat: function(query, values) { + queryFormat: function(this: mysql.Pool, query, values) { if (!values) { return query } <%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> - return query.replace(/\:(\w+)/g, function(matchedSubstring: string, capturedValue: string) { + return query.replace(/\:(\w+)/g, function(this: mysql.Pool, matchedSubstring: string, capturedValue: string) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) } From 1b6328abe37b9334604d1f3b91aea8a244b0692c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 11:56:07 +0700 Subject: [PATCH 190/346] refactor: split a step for showing versions into multiple steps Relate to #13 --- .github/workflows/integration-tests.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5a33b62..444de0d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -54,11 +54,14 @@ jobs: curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB sudo dpkg --install $DEB - - name: Show tools versions - run: | - hurl --version - docker compose version - docker version + - name: Show Hurl version + run: hurl --version + + - name: Show docker version + run: docker version + + - name: Show docker compose version + run: docker compose version - name: Start containers working-directory: docker From 85feeaa4c3f9a38f9cd44b731ddb45822064c143 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 12:04:56 +0700 Subject: [PATCH 191/346] chore: fix artifacts uploading from matrix jobs by giving a unique name to an artifact The error was: Error: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run See also: https://github.com/actions/upload-artifact/issues/480#issuecomment-1937762859 Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 444de0d..9e790d4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -104,5 +104,5 @@ jobs: if: failure() uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact with: - name: report-and-logs + name: ${{ matrix.docker-service-name }}-report-and-logs path: hurl-reports/ From c15739cd375c5b946dabd269366c395b68fd3b4e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 12:09:29 +0700 Subject: [PATCH 192/346] chore: save app and db logs to separate files Part of #13 --- .github/workflows/integration-tests.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 9e790d4..594f70a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -27,12 +27,16 @@ jobs: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations include: # "docker-service-name" must match "services.$name" from docker-compose.yaml + # "database-service-name" must match "services.$name" from docker-compose.yaml # "application-port" must match "services.$name.environment:PORT" from docker-compose.yaml - docker-service-name: 'express-js' + database-service-name: 'mysql' application-port: 3010 - docker-service-name: 'express-ts' + database-service-name: 'mysql' application-port: 3020 - docker-service-name: 'chi' + database-service-name: 'mysql' application-port: 3030 env: # Prevent interference between builds by setting the project name to a unique value. Otherwise @@ -86,10 +90,23 @@ jobs: --test \ tests/crud.hurl - - name: Save container logs + - name: Save application logs if: failure() working-directory: docker - run: docker compose logs --timestamps | tee ../hurl-reports/containers-logs.txt + run: >- + docker compose logs \ + --no-log-prefix \ + --timestamps \ + ${{ matrix.docker-service-name }} | tee ../hurl-reports/application-logs.txt + + - name: Save database logs + if: failure() + working-directory: docker + run: >- + docker compose logs \ + --no-log-prefix \ + --timestamps \ + ${{ matrix.database-service-name }} | tee ../hurl-reports/database-logs.txt - name: Stop containers if: always() From 7083cff400b2a6a48a509f1607594d4985614820 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 12:20:57 +0700 Subject: [PATCH 193/346] chore: don't show image pulling progress Part of #13 --- .github/workflows/integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 594f70a..d89d1c4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -74,6 +74,7 @@ jobs: --build \ --detach \ --wait \ + --quiet-pull \ ${{ matrix.docker-service-name }} - name: Show container statuses From 3e3c0379924d24da1d7359dcde856fa135b6e333 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 26 Mar 2024 13:11:53 +0700 Subject: [PATCH 194/346] feat(ts): handle internal server errors and avoid failing a whole app Part of #48 --- examples/ts/express/mysql/routes.ts | 34 ++++++++++++++++------------- src/templates/routes.ts.ejs | 22 +++++++++++-------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index f7e7e84..df1f384 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -1,14 +1,14 @@ -import { Express, Request, Response } from 'express' +import { Express, NextFunction, Request, Response } from 'express' import { Pool } from 'mysql' const register = (app: Express, pool: Pool) => { - app.get('/v1/categories/count', (req: Request, res: Response) => { + app.get('/v1/categories/count', (req: Request, res: Response, next: NextFunction) => { pool.query( 'SELECT COUNT(*) AS counter FROM categories', (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -19,13 +19,13 @@ const register = (app: Express, pool: Pool) => { ) }) - app.get('/v1/collections/:collectionId/categories/count', (req: Request, res: Response) => { + app.get('/v1/collections/:collectionId/categories/count', (req: Request, res: Response, next: NextFunction) => { pool.query( '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', { "collectionId": req.params.collectionId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -36,39 +36,39 @@ const register = (app: Express, pool: Pool) => { ) }) - app.get('/v1/categories', (req: Request, res: Response) => { + app.get('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', { "limit": req.query.limit }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.json(rows) } ) }) - app.post('/v1/categories', (req: Request, res: Response) => { + app.post('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) - app.get('/v1/categories/:categoryId', (req: Request, res: Response) => { + app.get('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } if (rows.length === 0) { res.status(404).end() @@ -79,32 +79,36 @@ const register = (app: Express, pool: Pool) => { ) }) - app.put('/v1/categories/:categoryId', (req: Request, res: Response) => { + app.put('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) - app.delete('/v1/categories/:categoryId', (req: Request, res: Response) => { + app.delete('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( 'DELETE FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } ) }) + app.use((error: any, req: Request, res: Response, next: NextFunction) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) + }) } exports.register = register; diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index 3ccd516..1c288ec 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -1,4 +1,4 @@ -import { Express, Request, Response } from 'express' +import { Express, NextFunction, Request, Response } from 'express' import { Pool } from 'mysql' const register = (app: Express, pool: Pool) => { @@ -21,12 +21,12 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> - app.get('<%- path %>', (req: Request, res: Response) => { + app.get('<%- path %>', (req: Request, res: Response, next: NextFunction) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } <% if (hasGetMany) { -%> res.json(rows) @@ -44,12 +44,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'post') { %> - app.post('<%- path %>', (req: Request, res: Response) => { + app.post('<%- path %>', (req: Request, res: Response, next: NextFunction) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -59,12 +59,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'put') { %> - app.put('<%- path %>', (req: Request, res: Response) => { + app.put('<%- path %>', (req: Request, res: Response, next: NextFunction) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -74,12 +74,12 @@ endpoints.forEach(function(endpoint) { } if (method.name === 'delete') { %> - app.delete('<%- path %>', (req: Request, res: Response) => { + app.delete('<%- path %>', (req: Request, res: Response, next: NextFunction) => { pool.query( '<%- sql %>',<%- formattedParams %> (err, rows, fields) => { if (err) { - throw err + return next(err) } res.sendStatus(204) } @@ -90,6 +90,10 @@ endpoints.forEach(function(endpoint) { }); }); %> + app.use((error: any, req: Request, res: Response, next: NextFunction) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) + }) } exports.register = register; From 8ac62f7ad502e0d5e11c7ce967364e9c80fb9d32 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 11:01:38 +0700 Subject: [PATCH 195/346] feat(golang): return JSON response for Internal Server Errors Part of #48 --- examples/go/chi/mysql/routes.go | 27 +++++++++++++++++---------- src/templates/routes.go.ejs | 19 ++++++++++++++----- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 0dbaaad..9719324 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -3,6 +3,7 @@ package main import "database/sql" import "encoding/json" import "fmt" +import "io" import "net/http" import "os" import "github.com/go-chi/chi" @@ -46,7 +47,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) } }) @@ -54,7 +55,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { stmt, 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") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -71,7 +72,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) } }) @@ -79,7 +80,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories LIMIT :limit") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -96,7 +97,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) } }) @@ -116,7 +117,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -127,7 +128,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -144,7 +145,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) } }) @@ -165,7 +166,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -182,7 +183,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -190,3 +191,9 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) } + +func internalServerError(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `{"error":"Internal Server Error"}`) +} diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 2a3d2f5..f6805bd 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -3,6 +3,7 @@ package main import "database/sql" import "encoding/json" import "fmt" +import "io" import "net/http" import "os" import "github.com/go-chi/chi" @@ -187,7 +188,7 @@ endpoints.forEach(function(endpoint) { stmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -208,7 +209,7 @@ endpoints.forEach(function(endpoint) { json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) } }) <% @@ -232,7 +233,7 @@ endpoints.forEach(function(endpoint) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -259,7 +260,7 @@ endpoints.forEach(function(endpoint) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -279,7 +280,7 @@ endpoints.forEach(function(endpoint) { ) if err != nil { fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) + internalServerError(w) return } @@ -291,3 +292,11 @@ endpoints.forEach(function(endpoint) { }) %> } + +<%# IMPORTANT: WriteHeader() must be called after w.Header() -%> +<%# w.Write() vs io.WriteString(): https://stackoverflow.com/questions/37863374/whats-the-difference-between-responsewriter-write-and-io-writestring -%> +func internalServerError(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `{"error":"Internal Server Error"}`) +} From 465d6fd00c013e25424d2f8f5134c5e537e5ba08 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 11:46:41 +0700 Subject: [PATCH 196/346] chore: don't run database container with docker compose Relate to #13 --- .github/workflows/integration-tests.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d89d1c4..61b824c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -27,20 +27,16 @@ jobs: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations include: # "docker-service-name" must match "services.$name" from docker-compose.yaml - # "database-service-name" must match "services.$name" from docker-compose.yaml # "application-port" must match "services.$name.environment:PORT" from docker-compose.yaml - docker-service-name: 'express-js' - database-service-name: 'mysql' application-port: 3010 - docker-service-name: 'express-ts' - database-service-name: 'mysql' application-port: 3020 - docker-service-name: 'chi' - database-service-name: 'mysql' application-port: 3030 env: # Prevent interference between builds by setting the project name to a unique value. Otherwise - # "docker compose down" has been stopping containers (especially database) from other builds. + # "docker compose down" has been stopping containers from other builds. # https://docs.docker.com/compose/project-name/ # https://docs.docker.com/compose/environment-variables/envvars/#compose_project_name COMPOSE_PROJECT_NAME: ${{ matrix.docker-service-name }} @@ -75,6 +71,7 @@ jobs: --detach \ --wait \ --quiet-pull \ + --no-deps \ ${{ matrix.docker-service-name }} - name: Show container statuses @@ -100,15 +97,6 @@ jobs: --timestamps \ ${{ matrix.docker-service-name }} | tee ../hurl-reports/application-logs.txt - - name: Save database logs - if: failure() - working-directory: docker - run: >- - docker compose logs \ - --no-log-prefix \ - --timestamps \ - ${{ matrix.database-service-name }} | tee ../hurl-reports/database-logs.txt - - name: Stop containers if: always() working-directory: docker From cf3087b985f14249acfd6aaa600c3d8a44666ca2 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 11:56:32 +0700 Subject: [PATCH 197/346] chore: pre-create a directory for reports to make it exists even when integration tests weren't run When `docker compose up` has failed because a container couldn't startup, the integration tests step is skipped and directory hurl-reports wasn't created. In this case, "Save application logs" step also has been failing with error: tee: ../hurl-reports/application-logs.txt: No such file or directory Part of #13 --- .github/workflows/integration-tests.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 61b824c..d66ba5d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -63,6 +63,9 @@ jobs: - name: Show docker compose version run: docker compose version + - name: Create directory for reports + run: mkdir tests-reports + - name: Start containers working-directory: docker run: >- @@ -83,7 +86,7 @@ jobs: run: >- hurl \ --error-format long \ - --report-html hurl-reports \ + --report-html tests-reports \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --test \ tests/crud.hurl @@ -95,7 +98,7 @@ jobs: docker compose logs \ --no-log-prefix \ --timestamps \ - ${{ matrix.docker-service-name }} | tee ../hurl-reports/application-logs.txt + ${{ matrix.docker-service-name }} | tee ../tests-reports/application-logs.txt - name: Stop containers if: always() @@ -111,4 +114,4 @@ jobs: uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact with: name: ${{ matrix.docker-service-name }}-report-and-logs - path: hurl-reports/ + path: tests-reports/ From eaaa90d167c2e7527afb2bdca412b2f57c1b12d8 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 12:02:45 +0700 Subject: [PATCH 198/346] chore: save logs before stopping all containers For unknown reason, sometimes "Save report" step wasn't executed. Try to move it close to the step that was failing. Relate to #13 --- .github/workflows/integration-tests.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d66ba5d..bf65526 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -100,6 +100,13 @@ jobs: --timestamps \ ${{ matrix.docker-service-name }} | tee ../tests-reports/application-logs.txt + - name: Save report + if: failure() + uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact + with: + name: ${{ matrix.docker-service-name }}-report-and-logs + path: tests-reports/ + - name: Stop containers if: always() working-directory: docker @@ -108,10 +115,3 @@ jobs: --volumes \ --remove-orphans \ --rmi local - - - name: Save report - if: failure() - uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact - with: - name: ${{ matrix.docker-service-name }}-report-and-logs - path: tests-reports/ From f4a97cdfd981bcf0d207a83896a6e7c232d906a3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 12:10:37 +0700 Subject: [PATCH 199/346] revert: "chore: don't run database container with docker compose" This reverts commit 465d6fd00c013e25424d2f8f5134c5e537e5ba08. Relate to #13 --- .github/workflows/integration-tests.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index bf65526..ea16393 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -27,16 +27,20 @@ jobs: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations include: # "docker-service-name" must match "services.$name" from docker-compose.yaml + # "database-service-name" must match "services.$name" from docker-compose.yaml # "application-port" must match "services.$name.environment:PORT" from docker-compose.yaml - docker-service-name: 'express-js' + database-service-name: 'mysql' application-port: 3010 - docker-service-name: 'express-ts' + database-service-name: 'mysql' application-port: 3020 - docker-service-name: 'chi' + database-service-name: 'mysql' application-port: 3030 env: # Prevent interference between builds by setting the project name to a unique value. Otherwise - # "docker compose down" has been stopping containers from other builds. + # "docker compose down" has been stopping containers (especially database) from other builds. # https://docs.docker.com/compose/project-name/ # https://docs.docker.com/compose/environment-variables/envvars/#compose_project_name COMPOSE_PROJECT_NAME: ${{ matrix.docker-service-name }} @@ -74,7 +78,6 @@ jobs: --detach \ --wait \ --quiet-pull \ - --no-deps \ ${{ matrix.docker-service-name }} - name: Show container statuses @@ -100,6 +103,15 @@ jobs: --timestamps \ ${{ matrix.docker-service-name }} | tee ../tests-reports/application-logs.txt + - name: Save database logs + if: failure() + working-directory: docker + run: >- + docker compose logs \ + --no-log-prefix \ + --timestamps \ + ${{ matrix.database-service-name }} | tee ../tests-reports/database-logs.txt + - name: Save report if: failure() uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact From 6bbf95c2a638ea8eb22af166ef06bad51bd29f14 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 12:38:44 +0700 Subject: [PATCH 200/346] chore: allow to trigger integration tests manually Relate to #13 --- .github/workflows/integration-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ea16393..010962b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -2,6 +2,8 @@ name: Integration Tests on: push: + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatch + workflow_dispatch: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions permissions: From 5230d3cc2859735f751eb52ff22aac5e2a8c7179 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 12:40:00 +0700 Subject: [PATCH 201/346] chore: enable debug for docker compose Relate to #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 010962b..1edf1cc 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -75,7 +75,7 @@ jobs: - name: Start containers working-directory: docker run: >- - docker compose up \ + docker --debug compose up \ --build \ --detach \ --wait \ From 571e912a946eea11008e8cef8c20420d437227b5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 27 Mar 2024 12:45:47 +0700 Subject: [PATCH 202/346] chore: save docker daemon logs Relate to #13 --- .github/workflows/integration-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1edf1cc..b7b0f1d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -114,6 +114,10 @@ jobs: --timestamps \ ${{ matrix.database-service-name }} | tee ../tests-reports/database-logs.txt + - name: Save docker logs + if: failure() + run: journalctl --catalog --unit docker.service | tee tests-reports/docker-logs.txt + - name: Save report if: failure() uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact From 70de65c1192762e23b95cd93e2d83e884ccc0188 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 09:44:59 +0700 Subject: [PATCH 203/346] fix(golang): correct endpoints.yaml to generate DTO with a type that matches input type The error was: NamedExec failed: Error 1366: Incorrect integer value: '' for column 'created_by' at row 1 Relate to #9 Relate to #13 --- examples/go/chi/mysql/routes.go | 2 +- examples/js/express/mysql/endpoints.yaml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 9719324..ade1b6b 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -24,7 +24,7 @@ type CreateCategoryDto struct { Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` - UserId *string `json:"user_id" db:"user_id"` + UserId *int `json:"user_id" db:"user_id"` } type CategoryInfoDto struct { diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 14f6488..5b16fd1 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -67,6 +67,9 @@ ) dto: name: CreateCategoryDto + fields: + user_id: + type: integer - path: /v1/categories/:categoryId get: @@ -91,6 +94,10 @@ , updated_at = NOW() , updated_by = :b.user_id WHERE id = :p.categoryId + dto: + fields: + user_id: + type: integer delete: query: >- DELETE From ff02eb082776e994c608141686f1f66b54c6452a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 09:58:25 +0700 Subject: [PATCH 204/346] chore: show container statuses only when the previous job isn't canncelled Part of #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b7b0f1d..41873c7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: ${{ matrix.docker-service-name }} - name: Show container statuses - if: always() + if: '!cancelled()' working-directory: docker run: docker compose ps From 2482db2e25081ed5205c0f3a0191b0223e166112 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 11:54:34 +0700 Subject: [PATCH 205/346] fix(golang): fix generated code for get_list method The error was: Select failed: scannable dest type slice with >1 columns (4) in result Relate to #9 Relate to #13 --- examples/go/chi/mysql/routes.go | 2 +- src/templates/routes.go.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index ade1b6b..8368ccd 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -88,7 +88,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { args := map[string]interface{}{ "limit": r.URL.Query().Get("limit"), } - err = stmt.Get(&result, args) + err = stmt.Select(&result, args) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index f6805bd..5190350 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -196,7 +196,7 @@ endpoints.forEach(function(endpoint) { args := map[string]interface{}{ <%- /* LATER: extract */ params.map(p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)](p.substring(2))},`).join('\n\t\t\t') %> } - err = stmt.Get(&result, args) + err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> var result <%- dataType %> err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>") From 00ef4cd75d820881ff740ba7ec64309c8103bb64 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 12:13:52 +0700 Subject: [PATCH 206/346] fix(golang): fix get_list method to return [] instead of null for empty result set Relate to #9 Relate to #13 --- examples/go/chi/mysql/routes.go | 2 +- src/templates/routes.go.ejs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 8368ccd..ac4b7d7 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -84,7 +84,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { return } - var result []CategoryDto + result := []CategoryDto{} args := map[string]interface{}{ "limit": r.URL.Query().Get("limit"), } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 5190350..a9e16ee 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -175,7 +175,9 @@ endpoints.forEach(function(endpoint) { // LATER: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - const dataType = hasGetMany ? '[]' + dtoName : dtoName; + const resultVariableDeclaration = hasGetMany + ? `result := []${dtoName}\{\}` + : `var result ${dtoName}`; const queryFunction = hasGetOne ? 'Get' : 'Select'; // LATER: handle only particular method (get/post/put) @@ -192,13 +194,13 @@ endpoints.forEach(function(endpoint) { return } - var result <%- dataType %> + <%- resultVariableDeclaration %> args := map[string]interface{}{ <%- /* LATER: extract */ params.map(p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)](p.substring(2))},`).join('\n\t\t\t') %> } err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> - var result <%- dataType %> + <%- resultVariableDeclaration %> err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>") <% } -%> switch err { From d16cb2ea4e032b7141ce782ee86baa357f2647b6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 12:19:34 +0700 Subject: [PATCH 207/346] revert: "chore: enable debug for docker compose" This reverts commit 5230d3cc2859735f751eb52ff22aac5e2a8c7179. Relate to #13 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 41873c7..dda4547 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -75,7 +75,7 @@ jobs: - name: Start containers working-directory: docker run: >- - docker --debug compose up \ + docker compose up \ --build \ --detach \ --wait \ From 64134c1c68eb7fee952aad0eb6b73427c00894d1 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 28 Mar 2024 12:20:04 +0700 Subject: [PATCH 208/346] revert: "chore: save docker daemon logs" This reverts commit 571e912a946eea11008e8cef8c20420d437227b5. Relate to #13 --- .github/workflows/integration-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index dda4547..ffdfe15 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -114,10 +114,6 @@ jobs: --timestamps \ ${{ matrix.database-service-name }} | tee ../tests-reports/database-logs.txt - - name: Save docker logs - if: failure() - run: journalctl --catalog --unit docker.service | tee tests-reports/docker-logs.txt - - name: Save report if: failure() uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact From e9d4ca1c9e08fc688778795971e000254ac159f5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 29 Mar 2024 10:57:27 +0700 Subject: [PATCH 209/346] ci: run integration tests for FastAPI/Python/PostgreSQL Part of #13 --- .github/workflows/integration-tests.yml | 3 +++ docker/docker-compose.yaml | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ffdfe15..f860cd8 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -40,6 +40,9 @@ jobs: - docker-service-name: 'chi' database-service-name: 'mysql' application-port: 3030 + - docker-service-name: 'fastapi' + database-service-name: 'postgres' + application-port: 4040 env: # Prevent interference between builds by setting the project name to a unique value. Otherwise # "docker compose down" has been stopping containers (especially database) from other builds. diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 2bf4494..71e792e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -27,6 +27,15 @@ services: retries: 10 start_period: 1s + postgres: + # https://hub.docker.com/_/postgres + image: postgres:12-bookworm + environment: + - POSTGRES_USER=test + - POSTGRES_PASSWORD=test + - POSTGRES_DB=test + volumes: + - ./categories.postgres.sql:/docker-entrypoint-initdb.d/categories.sql express-js: build: ../examples/js/express/mysql @@ -69,3 +78,17 @@ services: depends_on: mysql: condition: service_healthy + + fastapi: + build: ../examples/python/fastapi/postgres + environment: + - DB_NAME=test + - DB_USER=test + - DB_PASSWORD=test + - DB_HOST=postgres # defaults to localhost + - PORT=4040 # defaults to 3000 + ports: + - '4040:4040' + depends_on: + postgres: + condition: service_healthy From c9753d8425f936429b850420a9e9f33c59a42543 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 29 Mar 2024 10:59:07 +0700 Subject: [PATCH 210/346] chore: update docker compose examples --- docker/docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 71e792e..490a6c8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,7 +1,7 @@ # Usage example: # -# $ docker-compose up -d -# $ docker-compose exec mysql mysql -u test -ptest test +# $ docker compose up -d +# $ docker compose exec mysql mysql -u test -ptest test # version: '3' From 4e2f0760499480c2d2f631db27b56898b931ce7b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 29 Mar 2024 11:08:23 +0700 Subject: [PATCH 211/346] refactor: move steps for setting up hurl, closer to its usage Relate to #13 --- .github/workflows/integration-tests.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f860cd8..d74bcfb 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -57,24 +57,12 @@ jobs: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false - - name: Install Hurl - run: | - DEB=hurl_4.2.0_amd64.deb - curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB - sudo dpkg --install $DEB - - - name: Show Hurl version - run: hurl --version - - name: Show docker version run: docker version - name: Show docker compose version run: docker compose version - - name: Create directory for reports - run: mkdir tests-reports - - name: Start containers working-directory: docker run: >- @@ -90,6 +78,18 @@ jobs: working-directory: docker run: docker compose ps + - name: Install Hurl + run: | + DEB=hurl_4.2.0_amd64.deb + curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB + sudo dpkg --install $DEB + + - name: Show Hurl version + run: hurl --version + + - name: Create directory for reports + run: mkdir tests-reports + - name: Run integration tests run: >- hurl \ From 1abdf9b015f72e62354c45b662353fb030801bdb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 29 Mar 2024 11:23:14 +0700 Subject: [PATCH 212/346] chore: fix running of fastapi container because postgres didn't have a healthcheck The error was: dependency failed to start: container fastapi-postgres-1 has no healthcheck configured Part of #13 --- docker/docker-compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 490a6c8..2af6c95 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -36,6 +36,12 @@ services: - POSTGRES_DB=test volumes: - ./categories.postgres.sql:/docker-entrypoint-initdb.d/categories.sql + # Note: double dollar sign protects variables from docker compose interpolation + test: "pg_isready --user $$POSTGRES_USER --quiet --timeout 0" + interval: 1s + timeout: 5s + retries: 10 + start_period: 1s express-js: build: ../examples/js/express/mysql From 77b1a26667c91ac746fd458c6948800498056a88 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 29 Mar 2024 11:26:00 +0700 Subject: [PATCH 213/346] chore: fix docker-compose.yml syntax The error was: parsing docker-compose.yaml: yaml: line 37: did not find expected '-' indicator Correction for 1abdf9b015f72e62354c45b662353fb030801bdb commit. Relate to #13 --- docker/docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 2af6c95..0da83e8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -36,6 +36,7 @@ services: - POSTGRES_DB=test volumes: - ./categories.postgres.sql:/docker-entrypoint-initdb.d/categories.sql + healthcheck: # Note: double dollar sign protects variables from docker compose interpolation test: "pg_isready --user $$POSTGRES_USER --quiet --timeout 0" interval: 1s From f001ce45a1893cb66af62d8cd519d4c58318e3bc Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 1 Apr 2024 09:54:07 +0700 Subject: [PATCH 214/346] chore: correct internal port for FastAPI Part of #13 --- docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0da83e8..f4664b4 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -95,7 +95,7 @@ services: - DB_HOST=postgres # defaults to localhost - PORT=4040 # defaults to 3000 ports: - - '4040:4040' + - '4040:8000' depends_on: postgres: condition: service_healthy From 4d7a079426cd67aba7929659685c5f04e41d21a3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 1 Apr 2024 09:54:47 +0700 Subject: [PATCH 215/346] chore: add docker-compose.local.yaml for local development --- docker/docker-compose.local.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docker/docker-compose.local.yaml diff --git a/docker/docker-compose.local.yaml b/docker/docker-compose.local.yaml new file mode 100644 index 0000000..93249e9 --- /dev/null +++ b/docker/docker-compose.local.yaml @@ -0,0 +1,21 @@ +# Customize configuration from docker-compose.yaml to run services locally. +# +# In order to get the effective configuration, run +# docker compose -f docker-compose.yaml -f docker-compose.local.yaml config +# +# Usage examples: +# +# docker compose -f docker-compose.yaml -f docker-compose.local.yaml up -d +# docker compose exec mysql mysql -u test -ptest test -e 'SELECT * FROM categories' +# docker compose exec postgres psql -U test -c 'SELECT * FROM categories' +# +version: '3' + +services: + mysql: + ports: + - '3306:3306' + + postgres: + ports: + - '5432:5432' From 1e4de1823dd0ba5fb4d54d5d07ed9bad1a46eee6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 1 Apr 2024 09:56:54 +0700 Subject: [PATCH 216/346] chore: move usage examples to main docker compose file [skip ci] --- docker/docker-compose.local.yaml | 4 +--- docker/docker-compose.yaml | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/docker-compose.local.yaml b/docker/docker-compose.local.yaml index 93249e9..3bedabb 100644 --- a/docker/docker-compose.local.yaml +++ b/docker/docker-compose.local.yaml @@ -3,11 +3,9 @@ # In order to get the effective configuration, run # docker compose -f docker-compose.yaml -f docker-compose.local.yaml config # -# Usage examples: +# Usage: # # docker compose -f docker-compose.yaml -f docker-compose.local.yaml up -d -# docker compose exec mysql mysql -u test -ptest test -e 'SELECT * FROM categories' -# docker compose exec postgres psql -U test -c 'SELECT * FROM categories' # version: '3' diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index f4664b4..2a8ca49 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,7 +1,8 @@ -# Usage example: +# Usage examples: # -# $ docker compose up -d -# $ docker compose exec mysql mysql -u test -ptest test +# docker compose up -d +# docker compose exec mysql mysql -u test -ptest test -e 'SELECT * FROM categories' +# docker compose exec postgres psql -U test -c 'SELECT * FROM categories' # version: '3' From 3b9b22f7df3756e08de797238bd61071979c1127 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 3 Apr 2024 08:26:07 +0700 Subject: [PATCH 217/346] chore(python): use "status" from fastapi instead of starlette See also: - https://fastapi.tiangolo.com/reference/status/ - https://fastapi.tiangolo.com/tutorial/response-status-code/ --- examples/python/fastapi/postgres/routes.py | 4 +--- src/templates/routes.py.ejs | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 1bce328..4bbd686 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -1,9 +1,7 @@ import psycopg2 import psycopg2.extras -from fastapi import APIRouter, Depends, HTTPException - -from starlette import status +from fastapi import APIRouter, Depends, HTTPException, status from db import db_connection diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 7099e7c..dc0eca4 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -1,9 +1,8 @@ import psycopg2 import psycopg2.extras -from fastapi import APIRouter, Depends, HTTPException - -from starlette import status +<%# https://fastapi.tiangolo.com/reference/status/ -%> +from fastapi import APIRouter, Depends, HTTPException, status from db import db_connection From 524a6a072e3e5bfae00dd329ff8b3884f8507c77 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 3 Apr 2024 08:29:44 +0700 Subject: [PATCH 218/346] chore(python): use a constant for 404 status code --- examples/python/fastapi/postgres/routes.py | 6 +++--- src/templates/routes.py.ejs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 4bbd686..460cd36 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -16,7 +16,7 @@ def get_v1_categories_count(conn=Depends(db_connection)): cur.execute("SELECT COUNT(*) AS counter FROM categories") result = cur.fetchone() if result is None: - raise HTTPException(status_code=404) + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) return result finally: conn.close() @@ -56,7 +56,7 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn=Depends """, {"collectionId": collectionId}) result = cur.fetchone() if result is None: - raise HTTPException(status_code=404) + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) return result finally: conn.close() @@ -102,7 +102,7 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): """, {"categoryId": categoryId}) result = cur.fetchone() if result is None: - raise HTTPException(status_code=404) + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) return result finally: conn.close() diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index dc0eca4..0805459 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -102,7 +102,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): cur.execute(<%- query.sql %><%- query.formattedParams %>) result = cur.fetchone() if result is None: - raise HTTPException(status_code=404) + raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) return result <% } } else { From 1c994736e693d06410592a98cbb93a7d5ec9f3e3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 3 Apr 2024 12:14:41 +0700 Subject: [PATCH 219/346] chore: don't output SQL queries when generating an app Make output cleaner. --- src/cli.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cli.js b/src/cli.js index 33ebb16..3194c8b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -170,7 +170,7 @@ const createEndpoints = async (destDir, { lang }, config) => { } endpoint.methods.forEach(method => { const verb = method.verb.toUpperCase() - console.log(`${verb} ${path}`) + console.log(`\t${verb} ${path}`) let queries = [] if (method.query) { @@ -178,10 +178,6 @@ const createEndpoints = async (destDir, { lang }, config) => { } else if (method.aggregated_queries) { queries = Object.values(method.aggregated_queries) } - queries.forEach(query => { - const sql = removePlaceholders(flattenQuery(removeComments(query))) - console.log(`\t${sql}`) - }) }) } From d6d3e807a9041e22738721c332038212987da414 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 3 Apr 2024 13:10:09 +0700 Subject: [PATCH 220/346] chore(python): generate model for POST endpoints Part of #16 --- examples/python/fastapi/postgres/routes.py | 10 +- src/templates/routes.py.ejs | 121 ++++++++++++++++++++- 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 460cd36..09a4d73 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -3,10 +3,18 @@ from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel + from db import db_connection router = APIRouter() +class CreateCategoryDto(BaseModel): + name: str + name_ru: str + slug: str + user_id: int + @router.get('/v1/categories/count') def get_v1_categories_count(conn=Depends(db_connection)): @@ -82,7 +90,7 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)): @router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) -def post_v1_categories(): +def post_v1_categories(payload: CreateCategoryDto): pass diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 0805459..0861d6a 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -4,6 +4,9 @@ import psycopg2.extras <%# https://fastapi.tiangolo.com/reference/status/ -%> from fastapi import APIRouter, Depends, HTTPException, status +<%# LATER: add only when POST/PUT endpoints are present -%> +from pydantic import BaseModel + from db import db_connection router = APIRouter() @@ -38,7 +41,119 @@ function formatQueryForPython(query, indentLevel) { return `"${sql}"` } +// {'values': +// [ +// { +// type: 'expr_list', +// value: [ { type: 'param', value: 'user_id' } ] +// } +// ] +// } => [ 'user_id' ] +function extractInsertValues(queryAst) { + const values = queryAst.values.flatMap(elem => elem.value) + .map(elem => elem.type === 'param' ? elem.value : null) + .filter(elem => elem); // filter out nulls + return Array.from(new Set(values)); +} + +// LATER: reduce duplication with routes.go.ejs +function extractProperties(queryAst) { + if (queryAst.type === 'insert') { + return extractInsertValues(queryAst); + } + return []; +} + +// LATER: try to reduce duplication with routes.go.ejs +function findOutType(fieldsInfo, fieldName) { + const defaultType = 'str'; + const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type'); + if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { + return 'int'; + } + return defaultType; +} + +// LATER: reduce duplication with routes.go.ejs +function addTypes(props, fieldsInfo) { + return props.map(prop => { + return { + "name": prop, + "type": findOutType(fieldsInfo, prop), + } + }); +} + +// LATER: reduce duplication with routes.go.ejs +function query2dto(parser, method) { + const query = removePlaceholders(method.query); + const queryAst = parser.astify(query); + const props = extractProperties(queryAst); + if (props.length === 0) { + console.warn('Could not create DTO for query:', formatQuery(query)); + console.debug('Query AST:'); + console.debug(queryAst); + return null; + } + const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {}; + const propsWithTypes = addTypes(props, fieldsInfo); + const hasName = method.dto && method.dto.name && method.dto.name.length > 0; + const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; + return { + "name": name, + "hasUserProvidedName": hasName, + "props": propsWithTypes, + // required for de-duplication + // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" + // LATER: sort before join + "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') + }; +} + +// https://fastapi.tiangolo.com/tutorial/body/ +function dto2model(dto) { + let result = `class ${dto.name}(BaseModel):\n`; + dto.props.forEach(prop => { + result += ` ${prop.name}: ${prop.type}\n` + }); + return result; +} + +let globalDtoCounter = 0; +const dtoCache = {}; + +// LATER: reduce duplication with routes.go.ejs +function cacheDto(dto) { + dtoCache[dto.signature] = dto.name; + return dto; +} + +// LATER: reduce duplication with routes.go.ejs +function dtoInCache(dto) { + // always prefer user specified name even when we have a similar DTO in cache + if (dto.hasUserProvidedName) { + return false; + } + return dtoCache.hasOwnProperty(dto.signature); +} +// Generate models +const verbs_with_dto = [ 'post' ] +endpoints.forEach(function(endpoint) { + const dtos = endpoint.methods + .filter(method => verbs_with_dto.includes(method.verb)) + .map(method => query2dto(sqlParser, method)) + .filter(elem => elem) // filter out nulls + .filter(dto => !dtoInCache(dto)) + .map(dto => dto2model(cacheDto(dto))) + .forEach(model => { +%> +<%- model -%> +<% + }); +}); + +// Generate endpoints endpoints.forEach(function(endpoint) { const path = convertToFastApiPath(endpoint.path) const argsFromPath = extractParamsFromPath(endpoint.path) @@ -119,10 +234,14 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% } if (method.name === 'post') { + const dto = query2dto(sqlParser, method); + // LATER: do we really need signature and cache? + const cacheKey = dto ? dto.signature : null; + const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; %> @router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) -def <%- pythonMethodName %>(): +def <%- pythonMethodName %>(payload: <%- model %>): pass <% From de155c350299c78aff15f01da913911a17c9be80 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 08:52:21 +0700 Subject: [PATCH 221/346] refactor(python): initialize variables needed only for GET in the corresponding branch Relate to #16 --- src/templates/routes.py.ejs | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 0861d6a..19eba8a 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -167,29 +167,29 @@ endpoints.forEach(function(endpoint) { const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) - const queriesWithNames = [] - if (method.query) { - queriesWithNames.push({ "result" : method.query }) - } else if (method.aggregated_queries) { - for (const [key, value] of Object.entries(method.aggregated_queries)) { - queriesWithNames.push({ [key]: value }) - } - } + if (hasGetOne || hasGetMany) { - const queries = [] - queriesWithNames.forEach(queryWithName => { - for (const [name, query] of Object.entries(queryWithName)) { - const sql = convertToPsycopgNamedArguments(formatQueryForPython(query, 20)) - const params = extractParamsFromQuery(query); - const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] - ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + '}' - : '' - queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) + const queriesWithNames = [] + if (method.query) { + queriesWithNames.push({ "result" : method.query }) + } else if (method.aggregated_queries) { + for (const [key, value] of Object.entries(method.aggregated_queries)) { + queriesWithNames.push({ [key]: value }) + } } - }) - if (hasGetOne || hasGetMany) { + const queries = [] + queriesWithNames.forEach(queryWithName => { + for (const [name, query] of Object.entries(queryWithName)) { + const sql = convertToPsycopgNamedArguments(formatQueryForPython(query, 20)) + const params = extractParamsFromQuery(query); + const formattedParams = params.length > 0 + // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] + ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + '}' + : '' + queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) + } + }) %> @router.get('<%- path %>') From 8b4574e8d8e584820c4f333cb0b00ad47cea637d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 08:53:57 +0700 Subject: [PATCH 222/346] chore(python): add a comment with links about using psycopg Relate to #16 --- src/templates/routes.py.ejs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 19eba8a..0d5d173 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -195,6 +195,11 @@ endpoints.forEach(function(endpoint) { @router.get('<%- path %>') def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): try: +<%# + https://www.psycopg.org/docs/usage.html#with-statement + https://www.psycopg.org/docs/extras.html#dictionary-like-cursor + https://stackoverflow.com/questions/45399347/dictcursor-vs-realdictcursor +-%> with conn: <% if (hasGetOne) { if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> From e220aa07866937e2758b834a77e7d7748bde643a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 09:07:26 +0700 Subject: [PATCH 223/346] refactor(python): simplify code for generating GET endpoints Relate to #16 --- src/templates/routes.py.ejs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 0d5d173..b902004 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -201,37 +201,33 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): https://stackoverflow.com/questions/45399347/dictcursor-vs-realdictcursor -%> with conn: -<% if (hasGetOne) { - if (queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> +<% if (hasGetOne && queries.length > 1) { /* we can omit cursor_factory but in this case we might get an unused import */-%> with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: result = {} -<% queries.forEach(queryInfo => { - for (const [name, query] of Object.entries(queryInfo)) { +<% queries.forEach(queryInfo => { + for (const [name, query] of Object.entries(queryInfo)) { -%> cur.execute(<%- query.sql %><%- query.formattedParams %>) result['<%- name %>'] = cur.fetchone()[0] <% } - }) + }) -%> return result <% - } else { - const query = queries[0].result + } else { + const query = queries[0].result -%> with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(<%- query.sql %><%- query.formattedParams %>) +<% if (hasGetMany) { -%> + return cur.fetchall() +<% } else { /* GET with a single result */ -%> result = cur.fetchone() if result is None: raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) return result -<% } - } else { - const query = queries[0].result --%> - with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - cur.execute(<%- query.sql %><%- query.formattedParams %>) - return cur.fetchall() <% + } } -%> finally: From 4caebf2305cb3c72f1f07e84fb7d7fb18bd52530 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 09:29:50 +0700 Subject: [PATCH 224/346] feat(python): implement generation of POST endpoints Part of #16 --- examples/python/fastapi/postgres/routes.py | 31 ++++++++++++++++++++-- src/templates/routes.py.ejs | 18 +++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 09a4d73..9ff5be7 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -90,8 +90,35 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)): @router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) -def post_v1_categories(payload: CreateCategoryDto): - pass + +def post_v1_categories(payload: CreateCategoryDto, conn=Depends(db_connection)): + try: + with conn: + with conn.cursor() as cur: + cur.execute( + """ + INSERT + INTO categories + ( name + , name_ru + , slug + , created_at + , created_by + , updated_at + , updated_by + ) + VALUES + ( %(name)s + , %(name_ru)s + , %(slug)s + , NOW() + , %(user_id)s + , NOW() + , %(user_id)s + ) + """, {"name": payload.name, "name_ru": payload.name_ru, "slug": payload.slug, "user_id": payload.user_id, "user_id": payload.user_id}) + finally: + conn.close() @router.get('/v1/categories/{categoryId}') diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index b902004..b1d4700 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -239,11 +239,25 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): // LATER: do we really need signature and cache? const cacheKey = dto ? dto.signature : null; const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + + const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) + const params = extractParamsFromQuery(method.query); + const formattedParams = params.length > 0 + // [ "p.categoryId" ] => [ '"categoryId": payload.categoryId' ] + ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": payload.${param}`).join(', ') + '}' + : '' + %> @router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) -def <%- pythonMethodName %>(payload: <%- model %>): - pass +<%# LATER: deal with methodArgs %> +def <%- pythonMethodName %>(payload: <%- model %>, conn=Depends(db_connection)): + try: + with conn: + with conn.cursor() as cur: + cur.execute(<%- sql %><%- formattedParams %>) + finally: + conn.close() <% } From 67a664ab98f2b7c8204b88c4ce8653813113b9ed Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 19:29:42 +0700 Subject: [PATCH 225/346] chore(python): make all model fields optional by default Part of #16 Relate to #13 --- examples/python/fastapi/postgres/routes.py | 8 ++++---- src/templates/routes.py.ejs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 9ff5be7..a7e1cb5 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -10,10 +10,10 @@ router = APIRouter() class CreateCategoryDto(BaseModel): - name: str - name_ru: str - slug: str - user_id: int + name: str | None = None + name_ru: str | None = None + slug: str | None = None + user_id: int | None = None @router.get('/v1/categories/count') diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index b1d4700..93f81c4 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -114,7 +114,7 @@ function query2dto(parser, method) { function dto2model(dto) { let result = `class ${dto.name}(BaseModel):\n`; dto.props.forEach(prop => { - result += ` ${prop.name}: ${prop.type}\n` + result += ` ${prop.name}: ${prop.type} | None = None\n` }); return result; } From 7e1ff65699d0d25644aecee4849f41f8bdf95327 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 19:48:18 +0700 Subject: [PATCH 226/346] chore(python): unbreak optional fields on Python 3.7 The error was: TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' See https://stackoverflow.com/questions/76712720/typeerror-unsupported-operand-types-for-type-and-nonetype Correction for 67a664ab98f2b7c8204b88c4ce8653813113b9ed commit. Part of #16 Relate to #13 --- examples/python/fastapi/postgres/routes.py | 10 ++++++---- src/templates/routes.py.ejs | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index a7e1cb5..8c859b6 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -5,15 +5,17 @@ from pydantic import BaseModel +from typing import Optional + from db import db_connection router = APIRouter() class CreateCategoryDto(BaseModel): - name: str | None = None - name_ru: str | None = None - slug: str | None = None - user_id: int | None = None + name: Optional[str] = None + name_ru: Optional[str] = None + slug: Optional[str] = None + user_id: Optional[int] = None @router.get('/v1/categories/count') diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 93f81c4..176a40e 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -7,6 +7,9 @@ from fastapi import APIRouter, Depends, HTTPException, status <%# LATER: add only when POST/PUT endpoints are present -%> from pydantic import BaseModel +<%# LATER: add only when POST/PUT endpoints are present -%> +from typing import Optional + from db import db_connection router = APIRouter() @@ -114,7 +117,7 @@ function query2dto(parser, method) { function dto2model(dto) { let result = `class ${dto.name}(BaseModel):\n`; dto.props.forEach(prop => { - result += ` ${prop.name}: ${prop.type} | None = None\n` + result += ` ${prop.name}: Optional[${prop.type}] = None\n` }); return result; } From 4e22c127dbd6220c2c8323018997a70bfb7f02cf Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 4 Apr 2024 19:58:23 +0700 Subject: [PATCH 227/346] chore: don't save logs as artifact Part of #13 --- .github/workflows/integration-tests.yml | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d74bcfb..a8c288b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -7,8 +7,6 @@ on: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions permissions: - # NOTE: actions/upload-artifact makes no use of permissions - # See https://github.com/actions/upload-artifact/issues/197#issuecomment-832279436 contents: read # for "git clone" defaults: @@ -87,42 +85,31 @@ jobs: - name: Show Hurl version run: hurl --version - - name: Create directory for reports - run: mkdir tests-reports - - name: Run integration tests run: >- hurl \ --error-format long \ - --report-html tests-reports \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --test \ tests/crud.hurl - - name: Save application logs + - name: Show application logs if: failure() working-directory: docker run: >- docker compose logs \ --no-log-prefix \ --timestamps \ - ${{ matrix.docker-service-name }} | tee ../tests-reports/application-logs.txt + ${{ matrix.docker-service-name }} - - name: Save database logs + - name: Show database logs if: failure() working-directory: docker run: >- docker compose logs \ --no-log-prefix \ --timestamps \ - ${{ matrix.database-service-name }} | tee ../tests-reports/database-logs.txt - - - name: Save report - if: failure() - uses: actions/upload-artifact@v4.3.1 # https://github.com/actions/upload-artifact - with: - name: ${{ matrix.docker-service-name }}-report-and-logs - path: tests-reports/ + ${{ matrix.database-service-name }} - name: Stop containers if: always() From 1ece6c044f75b0d8048c0d7162d3e77696b11b41 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 5 Apr 2024 10:00:50 +0700 Subject: [PATCH 228/346] chore: relax check to not require "charset" for "application/json" content type Because FastAPI doesn't set "charset" parameter, this change should make the tests pass. RFC 4627 does not define a charset parameter because it requires that JSON is always encoded as Unicode (c) https://github.com/request/request/issues/383#issuecomment-18258693 Part of #13 --- tests/crud.hurl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/crud.hurl b/tests/crud.hurl index 7333368..19c6abd 100644 --- a/tests/crud.hurl +++ b/tests/crud.hurl @@ -28,7 +28,7 @@ jsonpath "$.counter" == 1 GET {{ SERVER_URL }}/v1/categories/1 HTTP 200 [Asserts] -header "Content-Type" == "application/json; charset=utf-8" +header "Content-Type" contains "application/json" jsonpath "$.id" == 1 jsonpath "$.name" == "Sport" jsonpath "$.name_ru" == null @@ -39,7 +39,7 @@ jsonpath "$.slug" == "sport" GET {{ SERVER_URL }}/v1/categories HTTP 200 [Asserts] -header "Content-Type" == "application/json; charset=utf-8" +header "Content-Type" contains "application/json" jsonpath "$" count == 1 jsonpath "$[0].id" == 1 jsonpath "$[0].name" == "Sport" @@ -61,7 +61,7 @@ HTTP 204 GET {{ SERVER_URL }}/v1/categories/1 HTTP 200 [Asserts] -header "Content-Type" == "application/json; charset=utf-8" +header "Content-Type" contains "application/json" jsonpath "$.name" == "Fauna" jsonpath "$.name_ru" == "Фауна" jsonpath "$.slug" == "fauna" From 3238fe887279b61c2bed58756c2f45daf3ae3c21 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 5 Apr 2024 10:05:23 +0700 Subject: [PATCH 229/346] chore(golang)!: don't set "charset=utf8" parameter for "Content-Type" RFC 4627 does not define a charset parameter because it requires that JSON is always encoded as Unicode (c) https://github.com/request/request/issues/383#issuecomment-18258693 --- examples/go/chi/mysql/routes.go | 10 +++++----- src/templates/routes.go.ejs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index ac4b7d7..b7527a3 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -43,7 +43,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) case nil: - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) @@ -68,7 +68,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) case nil: - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) @@ -93,7 +93,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) case nil: - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) @@ -141,7 +141,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) case nil: - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) @@ -193,7 +193,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { } func internalServerError(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) io.WriteString(w, `{"error":"Internal Server Error"}`) } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index a9e16ee..ed3b5b9 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -207,7 +207,7 @@ endpoints.forEach(function(endpoint) { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) case nil: - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(&result) default: fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err) @@ -298,7 +298,7 @@ endpoints.forEach(function(endpoint) { <%# IMPORTANT: WriteHeader() must be called after w.Header() -%> <%# w.Write() vs io.WriteString(): https://stackoverflow.com/questions/37863374/whats-the-difference-between-responsewriter-write-and-io-writestring -%> func internalServerError(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) io.WriteString(w, `{"error":"Internal Server Error"}`) } From 92ff16d7c4b4a4a0a343c91808a195277a9a82e1 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 10:45:52 +0700 Subject: [PATCH 230/346] chore: remove "limit" query parameter from get_list in order to make integration tests pass Partically reverts changes from f524f15523d423024feadc1234f96595d159ce3d commit. Reopen #23 Part of #13 Relate to #41 --- examples/go/chi/mysql/routes.go | 12 +----------- examples/js/express/mysql/endpoints.yaml | 3 +-- examples/js/express/mysql/routes.js | 3 +-- examples/python/fastapi/postgres/routes.py | 7 +++---- examples/ts/express/mysql/routes.ts | 3 +-- 5 files changed, 7 insertions(+), 21 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index b7527a3..7cbc875 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -77,18 +77,8 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories LIMIT :limit") - if err != nil { - fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - internalServerError(w) - return - } - result := []CategoryDto{} - args := map[string]interface{}{ - "limit": r.URL.Query().Get("limit"), - } - err = stmt.Select(&result, args) + err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories") switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 5b16fd1..b3652bb 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -37,8 +37,7 @@ , name , name_ru , slug - FROM categories - LIMIT :q.limit + FROM categories dto: name: CategoryDto fields: diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 0856eed..0da6fa1 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -35,8 +35,7 @@ const register = (app, pool) => { app.get('/v1/categories', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', - { "limit": req.query.limit }, + 'SELECT id , name , name_ru , slug FROM categories', (err, rows, fields) => { if (err) { return next(err) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 8c859b6..d7e36de 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -73,7 +73,7 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn=Depends @router.get('/v1/categories') -def get_list_v1_categories(limit, conn=Depends(db_connection)): +def get_list_v1_categories(conn=Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: @@ -83,9 +83,8 @@ def get_list_v1_categories(limit, conn=Depends(db_connection)): , name , name_ru , slug - FROM categories - LIMIT %(limit)s - """, {"limit": limit}) + FROM categories + """) return cur.fetchall() finally: conn.close() diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index df1f384..daab70b 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -38,8 +38,7 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories LIMIT :limit', - { "limit": req.query.limit }, + 'SELECT id , name , name_ru , slug FROM categories', (err, rows, fields) => { if (err) { return next(err) From 70c77a304d2b23e94c9ce808beacf4d5cff25f0c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 11:00:56 +0700 Subject: [PATCH 231/346] chore(python): remove an empty line from generated code Correction for 4caebf2305cb3c72f1f07e84fb7d7fb18bd52530 commit. Relate to #16 --- examples/python/fastapi/postgres/routes.py | 1 - src/templates/routes.py.ejs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index d7e36de..b9a7c73 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -91,7 +91,6 @@ def get_list_v1_categories(conn=Depends(db_connection)): @router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) - def post_v1_categories(payload: CreateCategoryDto, conn=Depends(db_connection)): try: with conn: diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 176a40e..9db2638 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -253,7 +253,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): %> @router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) -<%# LATER: deal with methodArgs %> +<%# LATER: deal with methodArgs -%> def <%- pythonMethodName %>(payload: <%- model %>, conn=Depends(db_connection)): try: with conn: From 8323f342162da175ae7aa83685dd0b57f7296807 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 11:03:34 +0700 Subject: [PATCH 232/346] refactor(python): rename "payload" to "body" Relate to #16 --- examples/python/fastapi/postgres/routes.py | 4 ++-- src/templates/routes.py.ejs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index b9a7c73..b0b9985 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -91,7 +91,7 @@ def get_list_v1_categories(conn=Depends(db_connection)): @router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) -def post_v1_categories(payload: CreateCategoryDto, conn=Depends(db_connection)): +def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): try: with conn: with conn.cursor() as cur: @@ -116,7 +116,7 @@ def post_v1_categories(payload: CreateCategoryDto, conn=Depends(db_connection)): , NOW() , %(user_id)s ) - """, {"name": payload.name, "name_ru": payload.name_ru, "slug": payload.slug, "user_id": payload.user_id, "user_id": payload.user_id}) + """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id, "user_id": body.user_id}) finally: conn.close() diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 9db2638..c2feb6b 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -246,15 +246,15 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) const params = extractParamsFromQuery(method.query); const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": payload.categoryId' ] - ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": payload.${param}`).join(', ') + '}' + // [ "p.categoryId" ] => [ '"categoryId": body.categoryId' ] + ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' : '' %> @router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) <%# LATER: deal with methodArgs -%> -def <%- pythonMethodName %>(payload: <%- model %>, conn=Depends(db_connection)): +def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): try: with conn: with conn.cursor() as cur: From ca5089a2c8780835550e12113df1cbfee2073f9b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 18:20:51 +0700 Subject: [PATCH 233/346] refactor(golang): rename local variable "dto" to "body" for POST and PUT endpoints --- examples/go/chi/mysql/routes.go | 24 ++++++++++++------------ src/cli.js | 2 +- src/templates/routes.go.ejs | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 7cbc875..a11acb2 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -92,14 +92,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - var dto CreateCategoryDto - json.NewDecoder(r.Body).Decode(&dto) + var body CreateCategoryDto + json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ - "name": dto.Name, - "name_ru": dto.NameRu, - "slug": dto.Slug, - "user_id": dto.UserId, + "name": body.Name, + "name_ru": body.NameRu, + "slug": body.Slug, + "user_id": body.UserId, } _, err := db.NamedExec( "INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )", @@ -140,14 +140,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - var dto CreateCategoryDto - json.NewDecoder(r.Body).Decode(&dto) + var body CreateCategoryDto + json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ - "name": dto.Name, - "name_ru": dto.NameRu, - "slug": dto.Slug, - "user_id": dto.UserId, + "name": body.Name, + "name_ru": body.NameRu, + "slug": body.Slug, + "user_id": body.UserId, "categoryId": chi.URLParam(r, "categoryId"), } _, err := db.NamedExec( diff --git a/src/cli.js b/src/cli.js index 3194c8b..db61edd 100755 --- a/src/cli.js +++ b/src/cli.js @@ -192,7 +192,7 @@ const createEndpoints = async (destDir, { lang }, config) => { return `chi.URLParam(r, "${param}")` }, 'b': function(param) { - return 'dto.' + capitalize(snake2camelCase(param)) + return 'body.' + capitalize(snake2camelCase(param)) }, 'q': function(param) { return `r.URL.Query().Get("${param}")` diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index ed3b5b9..11dac27 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -223,8 +223,8 @@ endpoints.forEach(function(endpoint) { const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var dto <%- dataType %> - json.NewDecoder(r.Body).Decode(&dto) + var body <%- dataType %> + json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ <%- formatParamsAsGolangMap(params) %> @@ -250,8 +250,8 @@ endpoints.forEach(function(endpoint) { const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var dto <%- dataType %> - json.NewDecoder(r.Body).Decode(&dto) + var body <%- dataType %> + json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ <%- formatParamsAsGolangMap(params) %> From 0dd5283fc1cac0a4237d1abbf046dc67aaeea03f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 18:51:49 +0700 Subject: [PATCH 234/346] refactor(python): extract method formatParamsAsPythonDict() --- src/cli.js | 8 ++++++++ src/templates/routes.py.ejs | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/cli.js b/src/cli.js index db61edd..d80e6f3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -272,6 +272,14 @@ const createEndpoints = async (destDir, { lang }, config) => { ).join('\n\t\t\t') }, + // [ "p.categoryId" ] => ', {"categoryId": body.categoryId}' + // (used only with Python) + "formatParamsAsPythonDict": (params) => { + return params.length > 0 + ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' + : '' + }, + "placeholdersMap": placeholdersMap, "removeComments": removeComments, } diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index c2feb6b..6d69cb9 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -245,10 +245,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) const params = extractParamsFromQuery(method.query); - const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": body.categoryId' ] - ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' - : '' + const formattedParams = formatParamsAsPythonDict(params) %> From 7e7abcf595db3bf3207ce55e1a81dd49ff0c4a1a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 18:56:30 +0700 Subject: [PATCH 235/346] chore(python): don't add duplicated values to dict Correction for 4caebf2305cb3c72f1f07e84fb7d7fb18bd52530 commit. Relate to #16 --- examples/python/fastapi/postgres/routes.py | 2 +- src/cli.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index b0b9985..cf0bd94 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -116,7 +116,7 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): , NOW() , %(user_id)s ) - """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id, "user_id": body.user_id}) + """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id}) finally: conn.close() diff --git a/src/cli.js b/src/cli.js index d80e6f3..50f93c3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -276,7 +276,7 @@ const createEndpoints = async (destDir, { lang }, config) => { // (used only with Python) "formatParamsAsPythonDict": (params) => { return params.length > 0 - ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' + ? ', {' + [...new Set(params)].map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' : '' }, From 98a1ed3aeae1d7b3c119ac4a3ea2dc6625fd4234 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 19:07:31 +0700 Subject: [PATCH 236/346] refactor(python): modify formatParamsAsPythonDict() to work with path and query parameters --- src/cli.js | 22 ++++++++++++++++++---- src/templates/routes.py.ejs | 5 +---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/cli.js b/src/cli.js index 50f93c3..689421d 100755 --- a/src/cli.js +++ b/src/cli.js @@ -197,7 +197,12 @@ const createEndpoints = async (destDir, { lang }, config) => { 'q': function(param) { return `r.URL.Query().Get("${param}")` }, - } + }, + 'py': { + 'p': '', + 'b': 'body.', + 'q': '', + }, } const parser = new Parser() @@ -275,9 +280,18 @@ const createEndpoints = async (destDir, { lang }, config) => { // [ "p.categoryId" ] => ', {"categoryId": body.categoryId}' // (used only with Python) "formatParamsAsPythonDict": (params) => { - return params.length > 0 - ? ', {' + [...new Set(params)].map(param => param.substring(2)).map(param => `"${param}": body.${param}`).join(', ') + '}' - : '' + 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['py'][bindTarget] + return `"${paramName}": ${prefix}${paramName}` + } + ).join(', ') + '}' }, "placeholdersMap": placeholdersMap, diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 6d69cb9..d8a035a 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -186,10 +186,7 @@ endpoints.forEach(function(endpoint) { for (const [name, query] of Object.entries(queryWithName)) { const sql = convertToPsycopgNamedArguments(formatQueryForPython(query, 20)) const params = extractParamsFromQuery(query); - const formattedParams = params.length > 0 - // [ "p.categoryId" ] => [ '"categoryId": categoryId' ] - ? ', {' + params.map(param => param.substring(2)).map(param => `"${param}": ${param}`).join(', ') + '}' - : '' + const formattedParams = formatParamsAsPythonDict(params) queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) } }) From 08a9909a9181662279ac26c6ba9479289b627f46 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 19:15:39 +0700 Subject: [PATCH 237/346] refactor(python): move GET specific code close to its usage --- src/templates/routes.py.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index d8a035a..eddd2ab 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -168,9 +168,9 @@ endpoints.forEach(function(endpoint) { // LATER: add support for aggregated_queries (#17) const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] - const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) if (hasGetOne || hasGetMany) { + const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) const queriesWithNames = [] if (method.query) { From 6547d7176793cff6839ebb857551a86005df2ba5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 11:23:05 +0700 Subject: [PATCH 238/346] feat(python): implement generation of PUT endpoints Part of #16 --- examples/python/fastapi/postgres/routes.py | 18 ++++++++-- src/templates/routes.py.ejs | 40 ++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index cf0bd94..7c07a98 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -144,8 +144,22 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): @router.put('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) -def put_v1_categories_category_id(): - pass +def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depends(db_connection)): + try: + with conn: + with conn.cursor() as cur: + cur.execute( + """ + UPDATE categories + SET name = %(name)s + , name_ru = %(name_ru)s + , slug = %(slug)s + , updated_at = NOW() + , updated_by = %(user_id)s + WHERE id = %(categoryId)s + """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id, "categoryId": categoryId}) + finally: + conn.close() @router.delete('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index eddd2ab..f3f547b 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -44,6 +44,7 @@ function formatQueryForPython(query, indentLevel) { return `"${sql}"` } +// LATER: reduce duplication with routes.go.ejs // {'values': // [ // { @@ -59,11 +60,30 @@ function extractInsertValues(queryAst) { return Array.from(new Set(values)); } +// LATER: reduce duplication with routes.go.ejs +// {'set': +// [ +// { +// column: 'updated_by', +// value: { type: 'param', value: 'user_id' }, +// table: null +// } +// ] +// } => [ 'user_id' ] +function extractUpdateValues(queryAst) { + // LATER: distinguish between b.param and q.param and extract only first + return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null) + .filter(value => value) // filter out nulls +} + // LATER: reduce duplication with routes.go.ejs function extractProperties(queryAst) { if (queryAst.type === 'insert') { return extractInsertValues(queryAst); } + if (queryAst.type === 'update') { + return extractUpdateValues(queryAst); + } return []; } @@ -259,11 +279,27 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): } if (method.name === 'put') { + // TODO: reduce duplication with POST + const dto = query2dto(sqlParser, method); + // LATER: do we really need signature and cache? + const cacheKey = dto ? dto.signature : null; + const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + + const methodArgs = [`body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)'] + + const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) + const params = extractParamsFromQuery(method.query); + const formattedParams = formatParamsAsPythonDict(params) %> @router.put('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) -def <%- pythonMethodName %>(): - pass +def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): + try: + with conn: + with conn.cursor() as cur: + cur.execute(<%- sql %><%- formattedParams %>) + finally: + conn.close() <% } From 9d7108cb304ba9c5896e5c3ac7dbb2c3b814c15e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 6 Apr 2024 18:32:19 +0700 Subject: [PATCH 239/346] refactor(python): remove spaces around equal sign --- examples/python/fastapi/postgres/routes.py | 12 ++++++------ src/templates/routes.py.ejs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 7c07a98..88abc5d 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -26,7 +26,7 @@ def get_v1_categories_count(conn=Depends(db_connection)): cur.execute("SELECT COUNT(*) AS counter FROM categories") result = cur.fetchone() if result is None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return result finally: conn.close() @@ -66,7 +66,7 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn=Depends """, {"collectionId": collectionId}) result = cur.fetchone() if result is None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return result finally: conn.close() @@ -90,7 +90,7 @@ def get_list_v1_categories(conn=Depends(db_connection)): conn.close() -@router.post('/v1/categories', status_code = status.HTTP_204_NO_CONTENT) +@router.post('/v1/categories', status_code=status.HTTP_204_NO_CONTENT) def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): try: with conn: @@ -137,13 +137,13 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): """, {"categoryId": categoryId}) result = cur.fetchone() if result is None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return result finally: conn.close() -@router.put('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) +@router.put('/v1/categories/{categoryId}', status_code=status.HTTP_204_NO_CONTENT) def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depends(db_connection)): try: with conn: @@ -162,6 +162,6 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe conn.close() -@router.delete('/v1/categories/{categoryId}', status_code = status.HTTP_204_NO_CONTENT) +@router.delete('/v1/categories/{categoryId}', status_code=status.HTTP_204_NO_CONTENT) def delete_v1_categories_category_id(): pass diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index f3f547b..12548ee 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -244,7 +244,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% } else { /* GET with a single result */ -%> result = cur.fetchone() if result is None: - raise HTTPException(status_code = status.HTTP_404_NOT_FOUND) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return result <% } @@ -266,7 +266,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): %> -@router.post('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) +@router.post('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) <%# LATER: deal with methodArgs -%> def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): try: @@ -292,7 +292,7 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): const formattedParams = formatParamsAsPythonDict(params) %> -@router.put('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) +@router.put('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): try: with conn: @@ -306,7 +306,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): if (method.name === 'delete') { %> -@router.delete('<%- path %>', status_code = status.HTTP_204_NO_CONTENT) +@router.delete('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) def <%- pythonMethodName %>(): pass <% From f66c23f69630a734614e4dea028aedb13ccd5006 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:48:26 +0700 Subject: [PATCH 240/346] style(python): add spaces around array arguments --- src/templates/routes.py.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 12548ee..130c250 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -285,7 +285,7 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): const cacheKey = dto ? dto.signature : null; const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; - const methodArgs = [`body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)'] + const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) const params = extractParamsFromQuery(method.query); From 0c73d2bf084c8dd8b7a57a99558093dc5eb02a59 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:53:43 +0700 Subject: [PATCH 241/346] style(golang): remove trailing semicolons --- src/templates/routes.go.ejs | 106 ++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 11dac27..16857c3 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -20,7 +20,7 @@ import "github.com/jmoiron/sqlx" // } => [ 'nameRu' ] function extractSelectParameters(queryAst) { return queryAst.columns - .map(column => column.as !== null ? column.as : column.expr.column); + .map(column => column.as !== null ? column.as : column.expr.column) } // {'values': @@ -34,8 +34,8 @@ function extractSelectParameters(queryAst) { function extractInsertValues(queryAst) { const values = queryAst.values.flatMap(elem => elem.value) .map(elem => elem.type === 'param' ? elem.value : null) - .filter(elem => elem); // filter out nulls - return Array.from(new Set(values)); + .filter(elem => elem) // filter out nulls + return Array.from(new Set(values)) } // {'set': @@ -56,27 +56,27 @@ function extractUpdateValues(queryAst) { // LATER: consider taking into account b.params from WHERE clause function extractProperties(queryAst) { if (queryAst.type === 'select') { - return extractSelectParameters(queryAst); + return extractSelectParameters(queryAst) } if (queryAst.type === 'insert') { - return extractInsertValues(queryAst); + return extractInsertValues(queryAst) } if (queryAst.type === 'update') { - return extractUpdateValues(queryAst); + return extractUpdateValues(queryAst) } - return []; + return [] } function findOutType(fieldsInfo, fieldName) { - const defaultType = '*string'; - const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type'); + const defaultType = '*string' + const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { - return '*int'; + return '*int' } - return defaultType; + return defaultType } function addTypes(props, fieldsInfo) { @@ -85,23 +85,23 @@ function addTypes(props, fieldsInfo) { "name": prop, "type": findOutType(fieldsInfo, prop), } - }); + }) } function query2dto(parser, method) { - const query = removePlaceholders(method.query); - const queryAst = parser.astify(query); - const props = extractProperties(queryAst); + const query = removePlaceholders(method.query) + const queryAst = parser.astify(query) + const props = extractProperties(queryAst) if (props.length === 0) { - console.warn('Could not create DTO for query:', formatQuery(query)); - console.debug('Query AST:'); - console.debug(queryAst); - return null; + console.warn('Could not create DTO for query:', formatQuery(query)) + console.debug('Query AST:') + console.debug(queryAst) + return null } - const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {}; - const propsWithTypes = addTypes(props, fieldsInfo); - const hasName = method.dto && method.dto.name && method.dto.name.length > 0; - const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; + const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {} + const propsWithTypes = addTypes(props, fieldsInfo) + const hasName = method.dto && method.dto.name && method.dto.name.length > 0 + const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter return { "name": name, "hasUserProvidedName": hasName, @@ -112,33 +112,33 @@ function query2dto(parser, method) { // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" // LATER: sort before join "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') - }; + } } function dto2struct(dto) { - let result = `type ${dto.name} struct {\n`; + let result = `type ${dto.name} struct {\n` dto.props.forEach(prop => { - const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength); + const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength) result += `\t${fieldName} ${prop.type} \`json:"${prop.name}" db:"${prop.name}"\`\n` - }); - result += '}\n'; + }) + result += '}\n' - return result; + return result } -let globalDtoCounter = 0; +let globalDtoCounter = 0 -const dtoCache = {}; +const dtoCache = {} function cacheDto(dto) { - dtoCache[dto.signature] = dto.name; - return dto; + dtoCache[dto.signature] = dto.name + return dto } function dtoInCache(dto) { // always prefer user specified name even when we have a similar DTO in cache if (dto.hasUserProvidedName) { - return false; + return false } - return dtoCache.hasOwnProperty(dto.signature); + return dtoCache.hasOwnProperty(dto.signature) } const verbs_with_dto = [ 'get', 'post', 'put' ] @@ -154,32 +154,32 @@ endpoints.forEach(function(endpoint) { -%> <%- struct %> <% - }); -}); + }) +}) -%> func registerRoutes(r chi.Router, db *sqlx.DB) { <% endpoints.forEach(function(endpoint) { - const path = convertPathPlaceholders(endpoint.path); + const path = convertPathPlaceholders(endpoint.path) endpoint.methods.forEach(function(method) { if (!method.query) { // filter out aggregated_queries for a while (see #17) return } - const params = extractParamsFromQuery(method.query); - const hasGetOne = method.name === 'get'; - const hasGetMany = method.name === 'get_list'; + const params = extractParamsFromQuery(method.query) + const hasGetOne = method.name === 'get' + const hasGetMany = method.name === 'get_list' if (hasGetOne || hasGetMany) { - const dto = query2dto(sqlParser, method); + const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null; - const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const cacheKey = dto ? dto.signature : null + const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name const resultVariableDeclaration = hasGetMany ? `result := []${dtoName}\{\}` - : `var result ${dtoName}`; + : `var result ${dtoName}` - const queryFunction = hasGetOne ? 'Get' : 'Select'; + const queryFunction = hasGetOne ? 'Get' : 'Select' // LATER: handle only particular method (get/post/put) // LATER: include method/path into an error message %> @@ -217,10 +217,10 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'post') { - const dto = query2dto(sqlParser, method); + const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null; - const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const cacheKey = dto ? dto.signature : null + const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> @@ -244,10 +244,10 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'put') { - const dto = query2dto(sqlParser, method); + const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null; - const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const cacheKey = dto ? dto.signature : null + const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> @@ -290,7 +290,7 @@ endpoints.forEach(function(endpoint) { }) <% } - }); + }) }) %> } From 275673826284061221a44735d636a38d587136d6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:54:43 +0700 Subject: [PATCH 242/346] style(python): remove trailing semicolons --- src/templates/routes.py.ejs | 88 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 130c250..48c93b6 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -16,7 +16,7 @@ router = APIRouter() <% // { "get", "/v1/categories/:categoryId" } => "get_v1_categories_category_id" function generate_method_name(method, path) { - const name = camel2snakeCase(path).replace(/\//g, '_').replace(/[^_a-z0-9]/g, ''); + const name = camel2snakeCase(path).replace(/\//g, '_').replace(/[^_a-z0-9]/g, '') return `${method}${name}` } @@ -39,7 +39,7 @@ function formatQueryForPython(query, indentLevel) { if (isMultilineSql) { const indent = ' '.repeat(indentLevel) const indentedSql = sql.replace(/\n/g, '\n' + indent) - return `\n${indent}"""\n${indent}${indentedSql}\n${indent}"""`; + return `\n${indent}"""\n${indent}${indentedSql}\n${indent}"""` } return `"${sql}"` } @@ -56,8 +56,8 @@ function formatQueryForPython(query, indentLevel) { function extractInsertValues(queryAst) { const values = queryAst.values.flatMap(elem => elem.value) .map(elem => elem.type === 'param' ? elem.value : null) - .filter(elem => elem); // filter out nulls - return Array.from(new Set(values)); + .filter(elem => elem) // filter out nulls + return Array.from(new Set(values)) } // LATER: reduce duplication with routes.go.ejs @@ -79,22 +79,22 @@ function extractUpdateValues(queryAst) { // LATER: reduce duplication with routes.go.ejs function extractProperties(queryAst) { if (queryAst.type === 'insert') { - return extractInsertValues(queryAst); + return extractInsertValues(queryAst) } if (queryAst.type === 'update') { - return extractUpdateValues(queryAst); + return extractUpdateValues(queryAst) } - return []; + return [] } // LATER: try to reduce duplication with routes.go.ejs function findOutType(fieldsInfo, fieldName) { - const defaultType = 'str'; - const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type'); + const defaultType = 'str' + const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { - return 'int'; + return 'int' } - return defaultType; + return defaultType } // LATER: reduce duplication with routes.go.ejs @@ -104,24 +104,24 @@ function addTypes(props, fieldsInfo) { "name": prop, "type": findOutType(fieldsInfo, prop), } - }); + }) } // LATER: reduce duplication with routes.go.ejs function query2dto(parser, method) { - const query = removePlaceholders(method.query); - const queryAst = parser.astify(query); - const props = extractProperties(queryAst); + const query = removePlaceholders(method.query) + const queryAst = parser.astify(query) + const props = extractProperties(queryAst) if (props.length === 0) { - console.warn('Could not create DTO for query:', formatQuery(query)); - console.debug('Query AST:'); - console.debug(queryAst); - return null; + console.warn('Could not create DTO for query:', formatQuery(query)) + console.debug('Query AST:') + console.debug(queryAst) + return null } - const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {}; - const propsWithTypes = addTypes(props, fieldsInfo); - const hasName = method.dto && method.dto.name && method.dto.name.length > 0; - const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter; + const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {} + const propsWithTypes = addTypes(props, fieldsInfo) + const hasName = method.dto && method.dto.name && method.dto.name.length > 0 + const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter return { "name": name, "hasUserProvidedName": hasName, @@ -130,34 +130,34 @@ function query2dto(parser, method) { // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" // LATER: sort before join "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') - }; + } } // https://fastapi.tiangolo.com/tutorial/body/ function dto2model(dto) { - let result = `class ${dto.name}(BaseModel):\n`; + let result = `class ${dto.name}(BaseModel):\n` dto.props.forEach(prop => { result += ` ${prop.name}: Optional[${prop.type}] = None\n` - }); - return result; + }) + return result } -let globalDtoCounter = 0; -const dtoCache = {}; +let globalDtoCounter = 0 +const dtoCache = {} // LATER: reduce duplication with routes.go.ejs function cacheDto(dto) { - dtoCache[dto.signature] = dto.name; - return dto; + dtoCache[dto.signature] = dto.name + return dto } // LATER: reduce duplication with routes.go.ejs function dtoInCache(dto) { // always prefer user specified name even when we have a similar DTO in cache if (dto.hasUserProvidedName) { - return false; + return false } - return dtoCache.hasOwnProperty(dto.signature); + return dtoCache.hasOwnProperty(dto.signature) } // Generate models @@ -173,8 +173,8 @@ endpoints.forEach(function(endpoint) { %> <%- model -%> <% - }); -}); + }) +}) // Generate endpoints endpoints.forEach(function(endpoint) { @@ -205,7 +205,7 @@ endpoints.forEach(function(endpoint) { queriesWithNames.forEach(queryWithName => { for (const [name, query] of Object.entries(queryWithName)) { const sql = convertToPsycopgNamedArguments(formatQueryForPython(query, 20)) - const params = extractParamsFromQuery(query); + const params = extractParamsFromQuery(query) const formattedParams = formatParamsAsPythonDict(params) queries.push({ [name]: { sql : sql, formattedParams: formattedParams }}) } @@ -255,13 +255,13 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% } if (method.name === 'post') { - const dto = query2dto(sqlParser, method); + const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null; - const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const cacheKey = dto ? dto.signature : null + const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) - const params = extractParamsFromQuery(method.query); + const params = extractParamsFromQuery(method.query) const formattedParams = formatParamsAsPythonDict(params) %> @@ -280,15 +280,15 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): } if (method.name === 'put') { // TODO: reduce duplication with POST - const dto = query2dto(sqlParser, method); + const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null; - const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name; + const cacheKey = dto ? dto.signature : null + const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) - const params = extractParamsFromQuery(method.query); + const params = extractParamsFromQuery(method.query) const formattedParams = formatParamsAsPythonDict(params) %> From cb5b9412db0be7fffc5ae10894582dd75ce963e5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:55:37 +0700 Subject: [PATCH 243/346] feat(python): implement generation of DELETE endpoints Part of #16 --- examples/python/fastapi/postgres/routes.py | 14 ++++++++++++-- src/templates/routes.py.ejs | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 88abc5d..1a4f90e 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -163,5 +163,15 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe @router.delete('/v1/categories/{categoryId}', status_code=status.HTTP_204_NO_CONTENT) -def delete_v1_categories_category_id(): - pass +def delete_v1_categories_category_id(categoryId, conn=Depends(db_connection)): + try: + with conn: + with conn.cursor() as cur: + cur.execute( + """ + DELETE + FROM categories + WHERE id = %(categoryId)s + """, {"categoryId": categoryId}) + finally: + conn.close() diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 48c93b6..011335b 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -304,11 +304,21 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } if (method.name === 'delete') { + const methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] + + const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) + const params = extractParamsFromQuery(method.query) + const formattedParams = formatParamsAsPythonDict(params) %> @router.delete('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) -def <%- pythonMethodName %>(): - pass +def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): + try: + with conn: + with conn.cursor() as cur: + cur.execute(<%- sql %><%- formattedParams %>) + finally: + conn.close() <% } From 0b5a73a8ba142e06579f14b9f7ae59884bd0f4b5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:57:54 +0700 Subject: [PATCH 244/346] refactor(js): remove trailing semicolons --- examples/js/express/mysql/routes.js | 2 +- src/templates/routes.js.ejs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 0da6fa1..cac3fa5 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -107,4 +107,4 @@ const register = (app, pool) => { }) } -exports.register = register; +exports.register = register diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 6c9fd47..155867a 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -1,17 +1,17 @@ const register = (app, pool) => { <% endpoints.forEach(function(endpoint) { - const path = endpoint.path; + const path = endpoint.path endpoint.methods.forEach(function(method) { if (!method.query) { // filter out aggregated_queries for a while (see #17) return } - const hasGetOne = method.name === 'get'; - const hasGetMany = method.name === 'get_list'; - const sql = formatQuery(method.query); - const params = extractParamsFromQuery(method.query); + const hasGetOne = method.name === 'get' + const hasGetMany = method.name === 'get_list' + const sql = formatQuery(method.query) + const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' : '' @@ -84,8 +84,8 @@ endpoints.forEach(function(endpoint) { }) <% } - }); -}); + }) +}) %> app.use((error, req, res, next) => { console.error(error) @@ -93,4 +93,4 @@ endpoints.forEach(function(endpoint) { }) } -exports.register = register; +exports.register = register From 2074fbe1f083c9f85335832254c967d1946d46b5 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 11:58:09 +0700 Subject: [PATCH 245/346] refactor(ts): remove trailing semicolons --- examples/ts/express/mysql/routes.ts | 2 +- src/templates/routes.ts.ejs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index daab70b..945a6bc 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -110,4 +110,4 @@ const register = (app: Express, pool: Pool) => { }) } -exports.register = register; +exports.register = register diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index 1c288ec..86b96d8 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -4,17 +4,17 @@ import { Pool } from 'mysql' const register = (app: Express, pool: Pool) => { <% endpoints.forEach(function(endpoint) { - const path = endpoint.path; + const path = endpoint.path endpoint.methods.forEach(function(method) { if (!method.query) { // filter out aggregated_queries for a while (see #17) return } - const hasGetOne = method.name === 'get'; - const hasGetMany = method.name === 'get_list'; - const sql = formatQuery(method.query); - const params = extractParamsFromQuery(method.query); + const hasGetOne = method.name === 'get' + const hasGetMany = method.name === 'get_list' + const sql = formatQuery(method.query) + const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' : '' @@ -87,8 +87,8 @@ endpoints.forEach(function(endpoint) { }) <% } - }); -}); + }) +}) %> app.use((error: any, req: Request, res: Response, next: NextFunction) => { console.error(error) @@ -96,4 +96,4 @@ endpoints.forEach(function(endpoint) { }) } -exports.register = register; +exports.register = register From 60d3522608c98384402738a768afd797e2b2b32c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 12:18:54 +0700 Subject: [PATCH 246/346] refactor(python): format dict on multiple lines --- examples/python/fastapi/postgres/routes.py | 27 ++++++++++++++++++---- src/cli.js | 9 +++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 1a4f90e..218f766 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -63,7 +63,9 @@ def get_v1_collections_collection_id_categories_count(collectionId, conn=Depends JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = %(collectionId)s - """, {"collectionId": collectionId}) + """, { + "collectionId": collectionId + }) result = cur.fetchone() if result is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) @@ -116,7 +118,12 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): , NOW() , %(user_id)s ) - """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id}) + """, { + "name": body.name, + "name_ru": body.name_ru, + "slug": body.slug, + "user_id": body.user_id + }) finally: conn.close() @@ -134,7 +141,9 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): , slug FROM categories WHERE id = %(categoryId)s - """, {"categoryId": categoryId}) + """, { + "categoryId": categoryId + }) result = cur.fetchone() if result is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) @@ -157,7 +166,13 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe , updated_at = NOW() , updated_by = %(user_id)s WHERE id = %(categoryId)s - """, {"name": body.name, "name_ru": body.name_ru, "slug": body.slug, "user_id": body.user_id, "categoryId": categoryId}) + """, { + "name": body.name, + "name_ru": body.name_ru, + "slug": body.slug, + "user_id": body.user_id, + "categoryId": categoryId + }) finally: conn.close() @@ -172,6 +187,8 @@ def delete_v1_categories_category_id(categoryId, conn=Depends(db_connection)): DELETE FROM categories WHERE id = %(categoryId)s - """, {"categoryId": categoryId}) + """, { + "categoryId": categoryId + }) finally: conn.close() diff --git a/src/cli.js b/src/cli.js index 689421d..e3e01f0 100755 --- a/src/cli.js +++ b/src/cli.js @@ -283,15 +283,18 @@ const createEndpoints = async (destDir, { lang }, config) => { if (params.length === 0) { return params } - return ', {' + Array.from( + const indentLevel = 24 + const indent = ' '.repeat(indentLevel) + const closingIndent = ' '.repeat(indentLevel - 4) + return ', {\n' + Array.from( new Set(params), p => { const bindTarget = p.substring(0, 1) const paramName = p.substring(2) const prefix = placeholdersMap['py'][bindTarget] - return `"${paramName}": ${prefix}${paramName}` + return `${indent}"${paramName}": ${prefix}${paramName}` } - ).join(', ') + '}' + ).join(',\n') + `\n${closingIndent}}` }, "placeholdersMap": placeholdersMap, From 9bbf051557255947bc426bc8f9db2883c906cb75 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 7 Apr 2024 21:34:05 +0700 Subject: [PATCH 247/346] feat(python): add support for boolean type for body arguments Part of #49 --- docker/categories.postgres.sql | 1 + examples/js/express/mysql/endpoints.yaml | 9 +++++++++ examples/python/fastapi/postgres/routes.py | 8 ++++++++ src/templates/routes.py.ejs | 8 +++++++- tests/crud.hurl | 5 +++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docker/categories.postgres.sql b/docker/categories.postgres.sql index e575563..35335b1 100644 --- a/docker/categories.postgres.sql +++ b/docker/categories.postgres.sql @@ -3,6 +3,7 @@ CREATE TABLE categories ( name varchar(50) NOT NULL, name_ru varchar(50) DEFAULT NULL, slug varchar(50) NOT NULL, + hidden boolean, created_at timestamp NOT NULL, created_by bigint NOT NULL, updated_at timestamp NOT NULL, diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index b3652bb..52b148a 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -37,6 +37,7 @@ , name , name_ru , slug + , hidden FROM categories dto: name: CategoryDto @@ -50,6 +51,7 @@ ( name , name_ru , slug + , hidden , created_at , created_by , updated_at @@ -59,6 +61,7 @@ ( :b.name , :b.name_ru , :b.slug + , :b.hidden , NOW() , :b.user_id , NOW() @@ -69,6 +72,8 @@ fields: user_id: type: integer + hidden: + type: boolean - path: /v1/categories/:categoryId get: @@ -77,6 +82,7 @@ , name , name_ru , slug + , hidden FROM categories WHERE id = :p.categoryId dto: @@ -90,6 +96,7 @@ SET name = :b.name , name_ru = :b.name_ru , slug = :b.slug + , hidden = :b.hidden , updated_at = NOW() , updated_by = :b.user_id WHERE id = :p.categoryId @@ -97,6 +104,8 @@ fields: user_id: type: integer + hidden: + type: boolean delete: query: >- DELETE diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 218f766..01be31b 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -15,6 +15,7 @@ class CreateCategoryDto(BaseModel): name: Optional[str] = None name_ru: Optional[str] = None slug: Optional[str] = None + hidden: Optional[bool] = None user_id: Optional[int] = None @@ -85,6 +86,7 @@ def get_list_v1_categories(conn=Depends(db_connection)): , name , name_ru , slug + , hidden FROM categories """) return cur.fetchall() @@ -104,6 +106,7 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): ( name , name_ru , slug + , hidden , created_at , created_by , updated_at @@ -113,6 +116,7 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): ( %(name)s , %(name_ru)s , %(slug)s + , %(hidden)s , NOW() , %(user_id)s , NOW() @@ -122,6 +126,7 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): "name": body.name, "name_ru": body.name_ru, "slug": body.slug, + "hidden": body.hidden, "user_id": body.user_id }) finally: @@ -139,6 +144,7 @@ def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): , name , name_ru , slug + , hidden FROM categories WHERE id = %(categoryId)s """, { @@ -163,6 +169,7 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe SET name = %(name)s , name_ru = %(name_ru)s , slug = %(slug)s + , hidden = %(hidden)s , updated_at = NOW() , updated_by = %(user_id)s WHERE id = %(categoryId)s @@ -170,6 +177,7 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe "name": body.name, "name_ru": body.name_ru, "slug": body.slug, + "hidden": body.hidden, "user_id": body.user_id, "categoryId": categoryId }) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 011335b..2433941 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -91,9 +91,15 @@ function extractProperties(queryAst) { function findOutType(fieldsInfo, fieldName) { const defaultType = 'str' const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') - if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { + if (!hasTypeInfo) { + return defaultType + } + if (fieldsInfo[fieldName].type === 'integer') { return 'int' } + if (fieldsInfo[fieldName].type === 'boolean') { + return 'bool' + } return defaultType } diff --git a/tests/crud.hurl b/tests/crud.hurl index 19c6abd..9de3b96 100644 --- a/tests/crud.hurl +++ b/tests/crud.hurl @@ -13,6 +13,7 @@ POST {{ SERVER_URL }}/v1/categories { "name": "Sport", "slug": "sport", + "hidden": true, "user_id": 1 } HTTP 204 @@ -33,6 +34,7 @@ jsonpath "$.id" == 1 jsonpath "$.name" == "Sport" jsonpath "$.name_ru" == null jsonpath "$.slug" == "sport" +jsonpath "$.hidden" == true # GET should return a list of values @@ -45,6 +47,7 @@ jsonpath "$[0].id" == 1 jsonpath "$[0].name" == "Sport" jsonpath "$[0].name_ru" == null jsonpath "$[0].slug" == "sport" +jsonpath "$[0].hidden" == true # PUT should update an object @@ -53,6 +56,7 @@ PUT {{ SERVER_URL }}/v1/categories/1 "name": "Fauna", "name_ru": "Фауна", "slug": "fauna", + "hidden": false, "user_id": 1 } HTTP 204 @@ -65,6 +69,7 @@ header "Content-Type" contains "application/json" jsonpath "$.name" == "Fauna" jsonpath "$.name_ru" == "Фауна" jsonpath "$.slug" == "fauna" +jsonpath "$.hidden" == false # DELETE should remove an object From 9c537a6e48958a5dd2c81ea8b529d3ab2d3cf1bb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 8 Apr 2024 20:12:19 +0700 Subject: [PATCH 248/346] feat(golang): add support for boolean type for body arguments Part of #49 --- docker/categories.mysql.sql | 1 + examples/go/chi/mysql/routes.go | 13 +++++++++---- examples/js/express/mysql/endpoints.yaml | 4 ++++ src/templates/routes.go.ejs | 6 ++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docker/categories.mysql.sql b/docker/categories.mysql.sql index bffc83a..78ff33a 100644 --- a/docker/categories.mysql.sql +++ b/docker/categories.mysql.sql @@ -3,6 +3,7 @@ CREATE TABLE `categories` ( `name` varchar(50) NOT NULL, `name_ru` varchar(50) DEFAULT NULL, `slug` varchar(50) NOT NULL, + `hidden` boolean, `created_at` datetime NOT NULL, `created_by` int(11) NOT NULL, `updated_at` datetime NOT NULL, diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index a11acb2..6f94dff 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -18,12 +18,14 @@ type CategoryDto struct { Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` + Hidden *bool `json:"hidden" db:"hidden"` } type CreateCategoryDto struct { Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` + Hidden *bool `json:"hidden" db:"hidden"` UserId *int `json:"user_id" db:"user_id"` } @@ -32,6 +34,7 @@ type CategoryInfoDto struct { Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` + Hidden *bool `json:"hidden" db:"hidden"` } func registerRoutes(r chi.Router, db *sqlx.DB) { @@ -78,7 +81,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { result := []CategoryDto{} - err := db.Select(&result, "SELECT id , name , name_ru , slug FROM categories") + err := db.Select(&result, "SELECT id , name , name_ru , slug , hidden FROM categories") switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) @@ -99,10 +102,11 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { "name": body.Name, "name_ru": body.NameRu, "slug": body.Slug, + "hidden": body.Hidden, "user_id": body.UserId, } _, err := db.NamedExec( - "INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )", + "INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )", args, ) if err != nil { @@ -115,7 +119,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId") + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -147,11 +151,12 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { "name": body.Name, "name_ru": body.NameRu, "slug": body.Slug, + "hidden": body.Hidden, "user_id": body.UserId, "categoryId": chi.URLParam(r, "categoryId"), } _, err := db.NamedExec( - "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", + "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", args, ) if err != nil { diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 52b148a..3668ebd 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -44,6 +44,8 @@ fields: id: type: integer + hidden: + type: boolean post: query: >- INSERT @@ -90,6 +92,8 @@ fields: id: type: integer + hidden: + type: boolean put: query: >- UPDATE categories diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 16857c3..5bf02d6 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -73,9 +73,15 @@ function extractProperties(queryAst) { function findOutType(fieldsInfo, fieldName) { const defaultType = '*string' const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') + if (!hasTypeInfo) { + return defaultType + } if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { return '*int' } + if (hasTypeInfo && fieldsInfo[fieldName].type === 'boolean') { + return '*bool' + } return defaultType } From 2b28eaeecebe84769580e5aaa1f6f0377366bcd9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 8 Apr 2024 20:14:18 +0700 Subject: [PATCH 249/346] feat(js): add support for boolean type for body arguments Part of #49 --- examples/js/express/mysql/app.js | 7 +++++++ examples/js/express/mysql/routes.js | 12 ++++++------ src/templates/app.js.ejs | 8 ++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index 84b6729..bd5a057 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -22,6 +22,13 @@ const pool = mysql.createPool({ } return matchedSubstring }.bind(this)) + }, + // Support for conversion from TINYINT(1) to boolean (https://github.com/mysqljs/mysql#custom-type-casting) + typeCast: function(field, next) { + if (field.type === 'TINY' && field.length === 1) { + return field.string() === '1' + } + return next() } }) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index cac3fa5..d856927 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -35,7 +35,7 @@ const register = (app, pool) => { app.get('/v1/categories', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories', + 'SELECT id , name , name_ru , slug , hidden FROM categories', (err, rows, fields) => { if (err) { return next(err) @@ -47,8 +47,8 @@ const register = (app, pool) => { app.post('/v1/categories', (req, res, next) => { pool.query( - 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, + 'INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { return next(err) @@ -60,7 +60,7 @@ const register = (app, pool) => { app.get('/v1/categories/:categoryId', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', + 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -77,8 +77,8 @@ const register = (app, pool) => { app.put('/v1/categories/:categoryId', (req, res, next) => { pool.query( - 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { return next(err) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 24a4937..584d1ba 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -23,6 +23,14 @@ const pool = mysql.createPool({ } return matchedSubstring }.bind(this)) + }, +<%# LATER: add it only when there is at least one DTO with boolean type -%> + // Support for conversion from TINYINT(1) to boolean (https://github.com/mysqljs/mysql#custom-type-casting) + typeCast: function(field, next) { + if (field.type === 'TINY' && field.length === 1) { + return field.string() === '1' + } + return next() } }) From cebb89d4d5a8d799d1846140e10a53548ade492c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 8 Apr 2024 20:16:32 +0700 Subject: [PATCH 250/346] feat(ts): add support for boolean type for body arguments Part of #49 --- examples/ts/express/mysql/app.ts | 7 +++++++ examples/ts/express/mysql/routes.ts | 12 ++++++------ src/templates/app.ts.ejs | 8 ++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index 7cf4673..44b749c 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -23,6 +23,13 @@ const pool = mysql.createPool({ } return matchedSubstring }.bind(this)) + }, + // Support for conversion from TINYINT(1) to boolean (https://github.com/mysqljs/mysql#custom-type-casting) + typeCast: function(field, next) { + if (field.type === 'TINY' && field.length === 1) { + return field.string() === '1' + } + return next() } }) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 945a6bc..b3c6601 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -38,7 +38,7 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories', + 'SELECT id , name , name_ru , slug , hidden FROM categories', (err, rows, fields) => { if (err) { return next(err) @@ -50,8 +50,8 @@ const register = (app: Express, pool: Pool) => { app.post('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'INSERT INTO categories ( name , name_ru , slug , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , NOW() , :user_id , NOW() , :user_id )', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id }, + 'INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { return next(err) @@ -63,7 +63,7 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug FROM categories WHERE id = :categoryId', + 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -80,8 +80,8 @@ const register = (app: Express, pool: Pool) => { app.put('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { return next(err) diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index d079d84..96cf653 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -24,6 +24,14 @@ const pool = mysql.createPool({ } return matchedSubstring }.bind(this)) + }, +<%# LATER: add it only when there is at least one DTO with boolean type -%> + // Support for conversion from TINYINT(1) to boolean (https://github.com/mysqljs/mysql#custom-type-casting) + typeCast: function(field, next) { + if (field.type === 'TINY' && field.length === 1) { + return field.string() === '1' + } + return next() } }) From ca96c1f78adea45d7b1934bb58feaeea15011bac Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 8 Apr 2024 20:17:05 +0700 Subject: [PATCH 251/346] style: use EJS comments --- src/templates/app.js.ejs | 2 +- src/templates/app.ts.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 584d1ba..809d060 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -16,7 +16,7 @@ const pool = mysql.createPool({ if (!values) { return query } -<%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> +<%# See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> return query.replace(/\:(\w+)/g, function(matchedSubstring, capturedValue) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 96cf653..24289b4 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -17,7 +17,7 @@ const pool = mysql.createPool({ if (!values) { return query } -<%- // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> +<%# See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement -%> return query.replace(/\:(\w+)/g, function(this: mysql.Pool, matchedSubstring: string, capturedValue: string) { if (values.hasOwnProperty(capturedValue)) { return this.escape(values[capturedValue]) From 5b4738c78f661f556b1579bf3079639e6ef8d255 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 09:33:59 +0700 Subject: [PATCH 252/346] refactor(golang): simplify generated code by re-using dto by multiple endpoints --- examples/go/chi/mysql/routes.go | 10 +--------- examples/js/express/mysql/endpoints.yaml | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 6f94dff..85ce867 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -29,14 +29,6 @@ type CreateCategoryDto struct { UserId *int `json:"user_id" db:"user_id"` } -type CategoryInfoDto struct { - Id *int `json:"id" db:"id"` - Name *string `json:"name" db:"name"` - NameRu *string `json:"name_ru" db:"name_ru"` - Slug *string `json:"slug" db:"slug"` - Hidden *bool `json:"hidden" db:"hidden"` -} - func registerRoutes(r chi.Router, db *sqlx.DB) { r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { @@ -126,7 +118,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { return } - var result CategoryInfoDto + var result CategoryDto args := map[string]interface{}{ "categoryId": chi.URLParam(r, "categoryId"), } diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 3668ebd..4f4b4c5 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -88,7 +88,6 @@ FROM categories WHERE id = :p.categoryId dto: - name: CategoryInfoDto fields: id: type: integer From ff69f70b150bf4481c8ddf24822be0a8e4cd68d6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 09:43:18 +0700 Subject: [PATCH 253/346] chore: remove non-existent npm goals --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index fba602a..ede504f 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,6 @@ "src/**" ], "scripts": { - "start": "node src/index.js", - "test": "echo \"Error: no test specified\" && exit 1", "gen-js-example": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", "gen-ts-example": "cd examples/ts/express/mysql; ../../../../src/cli.js --lang ts", "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", From 584d9f06d493dafe022c2b2b56b3d18193a28c5f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 09:54:39 +0700 Subject: [PATCH 254/346] fix(golang): correct struct format when a member of type int/bool is present --- examples/go/chi/mysql/routes.go | 8 ++++---- src/templates/routes.go.ejs | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 85ce867..edba032 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -14,19 +14,19 @@ type CounterDto struct { } type CategoryDto struct { - Id *int `json:"id" db:"id"` + Id *int `json:"id" db:"id"` Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` - Hidden *bool `json:"hidden" db:"hidden"` + Hidden *bool `json:"hidden" db:"hidden"` } type CreateCategoryDto struct { Name *string `json:"name" db:"name"` NameRu *string `json:"name_ru" db:"name_ru"` Slug *string `json:"slug" db:"slug"` - Hidden *bool `json:"hidden" db:"hidden"` - UserId *int `json:"user_id" db:"user_id"` + Hidden *bool `json:"hidden" db:"hidden"` + UserId *int `json:"user_id" db:"user_id"` } func registerRoutes(r chi.Router, db *sqlx.DB) { diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 5bf02d6..7771254 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -112,8 +112,9 @@ function query2dto(parser, method) { "name": name, "hasUserProvidedName": hasName, "props": propsWithTypes, - // max length is needed for proper formatting + // max lengths are needed for proper formatting "maxFieldNameLength": lengthOfLongestString(props.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))), + "maxFieldTypeLength": lengthOfLongestString(propsWithTypes.map(el => el.type)), // required for de-duplication // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" // LATER: sort before join @@ -125,7 +126,8 @@ function dto2struct(dto) { let result = `type ${dto.name} struct {\n` dto.props.forEach(prop => { const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength) - result += `\t${fieldName} ${prop.type} \`json:"${prop.name}" db:"${prop.name}"\`\n` + const fieldType = prop.type.padEnd(dto.maxFieldTypeLength) + result += `\t${fieldName} ${fieldType} \`json:"${prop.name}" db:"${prop.name}"\`\n` }) result += '}\n' From 056eccebb10f3c01adb515168bd719cfbc1afb73 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 09:54:39 +0700 Subject: [PATCH 255/346] refactor(golang): extract method obtainDtoName() --- src/templates/routes.go.ejs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 7771254..9df6321 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -149,6 +149,11 @@ function dtoInCache(dto) { return dtoCache.hasOwnProperty(dto.signature) } +function obtainDtoName(dto) { + const cacheKey = dto ? dto.signature : null + return dtoInCache(dto) ? dtoCache[cacheKey] : dto.name +} + const verbs_with_dto = [ 'get', 'post', 'put' ] endpoints.forEach(function(endpoint) { const dtos = endpoint.methods @@ -181,8 +186,7 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null - const dtoName = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const dataType = obtainDtoName(dto) const resultVariableDeclaration = hasGetMany ? `result := []${dtoName}\{\}` : `var result ${dtoName}` @@ -227,8 +231,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'post') { const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null - const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const dataType = obtainDtoName(dto) %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> @@ -254,8 +257,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'put') { const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null - const dataType = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const dataType = obtainDtoName(dto) %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> From c3296ca07a8286d9a4e4900727e6b62cd35a5699 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 10:39:23 +0700 Subject: [PATCH 256/346] fix(golang): fix duplicated DTO with user-defined name --- src/templates/routes.go.ejs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 9df6321..7335de4 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -137,21 +137,28 @@ function dto2struct(dto) { let globalDtoCounter = 0 const dtoCache = {} +const namedDtoCache = {} + function cacheDto(dto) { - dtoCache[dto.signature] = dto.name + if (dto.hasUserProvidedName) { + namedDtoCache[dto.signature] = dto.name + } else { + dtoCache[dto.signature] = dto.name + } return dto } + function dtoInCache(dto) { - // always prefer user specified name even when we have a similar DTO in cache + // always prefer user specified name even when we have a similar DTO in cache for generated names if (dto.hasUserProvidedName) { - return false + return namedDtoCache.hasOwnProperty(dto.signature) } return dtoCache.hasOwnProperty(dto.signature) } function obtainDtoName(dto) { - const cacheKey = dto ? dto.signature : null - return dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const cacheKey = dto.signature + return namedDtoCache.hasOwnProperty(cacheKey) ? namedDtoCache[cacheKey] : dto.name } const verbs_with_dto = [ 'get', 'post', 'put' ] From 76fffb24c40e9387d675d353e26cbbd884456b7c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 10:49:25 +0700 Subject: [PATCH 257/346] fix(python): fix absent model when only PUT endpoint is present Correction for 6547d7176793cff6839ebb857551a86005df2ba5 commit. Relate to #16 --- src/templates/routes.py.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 2433941..36a6d18 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -167,7 +167,7 @@ function dtoInCache(dto) { } // Generate models -const verbs_with_dto = [ 'post' ] +const verbs_with_dto = [ 'post', 'put' ] endpoints.forEach(function(endpoint) { const dtos = endpoint.methods .filter(method => verbs_with_dto.includes(method.verb)) From 8c8b91aec7f1b06ec826f8c49cbd5973a3c95665 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 10:55:36 +0700 Subject: [PATCH 258/346] refactor(python): extract method obtainDtoName() --- src/templates/routes.py.ejs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 36a6d18..4bdfa09 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -166,6 +166,11 @@ function dtoInCache(dto) { return dtoCache.hasOwnProperty(dto.signature) } +function obtainDtoName(dto) { + const cacheKey = dto ? dto.signature : null + return dtoInCache(dto) ? dtoCache[cacheKey] : dto.name +} + // Generate models const verbs_with_dto = [ 'post', 'put' ] endpoints.forEach(function(endpoint) { @@ -263,8 +268,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): if (method.name === 'post') { const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null - const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const model = obtainDtoName(dto) const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) const params = extractParamsFromQuery(method.query) @@ -288,8 +292,7 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): // TODO: reduce duplication with POST const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? - const cacheKey = dto ? dto.signature : null - const model = dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const model = obtainDtoName(dto) const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] From 6d153379eb6ab383e51ae538b7ef32e9eca49ddd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 11:01:21 +0700 Subject: [PATCH 259/346] fix(python): fix duplicated DTO with user-defined name --- src/templates/routes.py.ejs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 4bdfa09..4d74485 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -150,25 +150,30 @@ function dto2model(dto) { let globalDtoCounter = 0 const dtoCache = {} +const namedDtoCache = {} // LATER: reduce duplication with routes.go.ejs function cacheDto(dto) { - dtoCache[dto.signature] = dto.name + if (dto.hasUserProvidedName) { + namedDtoCache[dto.signature] = dto.name + } else { + dtoCache[dto.signature] = dto.name + } return dto } // LATER: reduce duplication with routes.go.ejs function dtoInCache(dto) { - // always prefer user specified name even when we have a similar DTO in cache + // always prefer user specified name even when we have a similar DTO in cache for generated names if (dto.hasUserProvidedName) { - return false + return namedDtoCache.hasOwnProperty(dto.signature) } return dtoCache.hasOwnProperty(dto.signature) } function obtainDtoName(dto) { - const cacheKey = dto ? dto.signature : null - return dtoInCache(dto) ? dtoCache[cacheKey] : dto.name + const cacheKey = dto.signature + return namedDtoCache.hasOwnProperty(cacheKey) ? namedDtoCache[cacheKey] : dto.name } // Generate models From ebba278517104bb8e3606b84c5ef832bd49c2507 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 11:19:50 +0700 Subject: [PATCH 260/346] chore(golang): fix "dtoName is not defined" error Correction for 056eccebb10f3c01adb515168bd719cfbc1afb73 commit. --- src/templates/routes.go.ejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 7335de4..73d67bc 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -195,8 +195,8 @@ endpoints.forEach(function(endpoint) { // LATER: do we really need signature and cache? const dataType = obtainDtoName(dto) const resultVariableDeclaration = hasGetMany - ? `result := []${dtoName}\{\}` - : `var result ${dtoName}` + ? `result := []${dataType}\{\}` + : `var result ${dataType}` const queryFunction = hasGetOne ? 'Get' : 'Select' // LATER: handle only particular method (get/post/put) From a1edfb6d60067bc0abcfd4817874ece2492a1bb6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 9 Apr 2024 11:35:51 +0700 Subject: [PATCH 261/346] chore(golang.python): fix generation of useless models --- src/templates/routes.go.ejs | 6 ++++-- src/templates/routes.py.ejs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 73d67bc..246aed5 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -149,11 +149,13 @@ function cacheDto(dto) { } function dtoInCache(dto) { + const existsNamed = namedDtoCache.hasOwnProperty(dto.signature) // always prefer user specified name even when we have a similar DTO in cache for generated names if (dto.hasUserProvidedName) { - return namedDtoCache.hasOwnProperty(dto.signature) + return existsNamed } - return dtoCache.hasOwnProperty(dto.signature) + // prefer to re-use named DTO + return existsNamed || dtoCache.hasOwnProperty(dto.signature) } function obtainDtoName(dto) { diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 4d74485..b76f9ba 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -164,11 +164,13 @@ function cacheDto(dto) { // LATER: reduce duplication with routes.go.ejs function dtoInCache(dto) { + const existsNamed = namedDtoCache.hasOwnProperty(dto.signature) // always prefer user specified name even when we have a similar DTO in cache for generated names if (dto.hasUserProvidedName) { - return namedDtoCache.hasOwnProperty(dto.signature) + return existsNamed } - return dtoCache.hasOwnProperty(dto.signature) + // prefer to re-use named DTO + return existsNamed || dtoCache.hasOwnProperty(dto.signature) } function obtainDtoName(dto) { From db0be3e8b55459c6b7fdee9a65a1df5ac98d80bb Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:11:47 +0700 Subject: [PATCH 262/346] refactor(go): reduce code duplication by re-using existing method --- src/templates/routes.go.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 246aed5..6a02f87 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -217,7 +217,7 @@ endpoints.forEach(function(endpoint) { <%- resultVariableDeclaration %> args := map[string]interface{}{ - <%- /* LATER: extract */ params.map(p => `"${p.substring(2)}": ${placeholdersMap['go'][p.substring(0, 1)](p.substring(2))},`).join('\n\t\t\t') %> + <%- formatParamsAsGolangMap(params) %> } err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> From f5eee90801df2f09ff3159faf98beaa95d6d348b Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:26:45 +0700 Subject: [PATCH 263/346] refactor(python): extract code shared between post/put/delete --- src/templates/routes.py.ejs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index b76f9ba..28e23bd 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -207,6 +207,15 @@ endpoints.forEach(function(endpoint) { // LATER: add support for aggregated_queries (#17) const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] + // define before "if", to make them available later + let sql + let formattedParams + if (method.name === 'post' || method.name === 'put' || method.name === 'delete') { + sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) + const params = extractParamsFromQuery(method.query) + formattedParams = formatParamsAsPythonDict(params) + } + if (hasGetOne || hasGetMany) { const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) @@ -276,11 +285,6 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? const model = obtainDtoName(dto) - - const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) - const params = extractParamsFromQuery(method.query) - const formattedParams = formatParamsAsPythonDict(params) - %> @router.post('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) @@ -302,10 +306,6 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): const model = obtainDtoName(dto) const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] - - const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) - const params = extractParamsFromQuery(method.query) - const formattedParams = formatParamsAsPythonDict(params) %> @router.put('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) @@ -321,10 +321,6 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } if (method.name === 'delete') { const methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] - - const sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) - const params = extractParamsFromQuery(method.query) - const formattedParams = formatParamsAsPythonDict(params) %> @router.delete('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) From 79b84ce2c5e8e0166ebf49bd693c2b3bce833867 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:31:36 +0700 Subject: [PATCH 264/346] refactor(python): extract code shared between post/put --- src/templates/routes.py.ejs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 28e23bd..e687721 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -216,6 +216,13 @@ endpoints.forEach(function(endpoint) { formattedParams = formatParamsAsPythonDict(params) } + let model + if (method.name === 'post' || method.name === 'put') { + const dto = query2dto(sqlParser, method) + // LATER: do we really need signature and cache? + model = obtainDtoName(dto) + } + if (hasGetOne || hasGetMany) { const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) @@ -282,9 +289,6 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): <% } if (method.name === 'post') { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - const model = obtainDtoName(dto) %> @router.post('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) @@ -300,11 +304,6 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): } if (method.name === 'put') { - // TODO: reduce duplication with POST - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - const model = obtainDtoName(dto) - const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] %> From 43b78784b1353c1eb746519b9cffaca93e0dc1f3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:40:14 +0700 Subject: [PATCH 265/346] refactor(python): move shared code for initializing method args --- src/templates/routes.py.ejs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index e687721..3c8394b 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -208,6 +208,7 @@ endpoints.forEach(function(endpoint) { const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] // define before "if", to make them available later + let methodArgs let sql let formattedParams if (method.name === 'post' || method.name === 'put' || method.name === 'delete') { @@ -221,10 +222,15 @@ endpoints.forEach(function(endpoint) { const dto = query2dto(sqlParser, method) // LATER: do we really need signature and cache? model = obtainDtoName(dto) + methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] + } + + if (method.name === 'delete') { + methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] } if (hasGetOne || hasGetMany) { - const methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) + methodArgs = Array.from(new Set([...argsFromPath, ...argsFromQuery, 'conn=Depends(db_connection)'])) const queriesWithNames = [] if (method.query) { @@ -292,8 +298,7 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): %> @router.post('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) -<%# LATER: deal with methodArgs -%> -def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): +def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): try: with conn: with conn.cursor() as cur: @@ -304,7 +309,6 @@ def <%- pythonMethodName %>(body: <%- model %>, conn=Depends(db_connection)): } if (method.name === 'put') { - const methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] %> @router.put('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) @@ -319,7 +323,6 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>): } if (method.name === 'delete') { - const methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] %> @router.delete('<%- path %>', status_code=status.HTTP_204_NO_CONTENT) From dbd5b576d46af349da7fd721f39db0872717546a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:43:17 +0700 Subject: [PATCH 266/346] refactor(python): combine if-s --- src/templates/routes.py.ejs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 3c8394b..74debe5 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -211,22 +211,21 @@ endpoints.forEach(function(endpoint) { let methodArgs let sql let formattedParams + let model if (method.name === 'post' || method.name === 'put' || method.name === 'delete') { sql = convertToPsycopgNamedArguments(formatQueryForPython(method.query, 20)) const params = extractParamsFromQuery(method.query) formattedParams = formatParamsAsPythonDict(params) - } - let model - if (method.name === 'post' || method.name === 'put') { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - model = obtainDtoName(dto) - methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] - } + if (method.name === 'post' || method.name === 'put') { + const dto = query2dto(sqlParser, method) + // LATER: do we really need signature and cache? + model = obtainDtoName(dto) + methodArgs = [ `body: ${model}`, ...argsFromPath, 'conn=Depends(db_connection)' ] - if (method.name === 'delete') { - methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] + } else if (method.name === 'delete') { + methodArgs = [ ...argsFromPath, 'conn=Depends(db_connection)' ] + } } if (hasGetOne || hasGetMany) { From c8984414fc068fef1b8b48089ef3dc219d01cbd9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 11 Apr 2024 08:51:18 +0700 Subject: [PATCH 267/346] refactor(golang): extract code shared between get/get_list/post/put --- src/templates/routes.go.ejs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 6a02f87..76a279a 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -189,13 +189,19 @@ endpoints.forEach(function(endpoint) { // filter out aggregated_queries for a while (see #17) return } + + // define before "if", to make it available later + let dataType + if (method.name !== 'delete') { + const dto = query2dto(sqlParser, method) + // LATER: do we really need signature and cache? + dataType = obtainDtoName(dto) + } + const params = extractParamsFromQuery(method.query) const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' if (hasGetOne || hasGetMany) { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - const dataType = obtainDtoName(dto) const resultVariableDeclaration = hasGetMany ? `result := []${dataType}\{\}` : `var result ${dataType}` @@ -238,9 +244,6 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'post') { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - const dataType = obtainDtoName(dto) %> r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> @@ -264,9 +267,6 @@ endpoints.forEach(function(endpoint) { <% } if (method.name === 'put') { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - const dataType = obtainDtoName(dto) %> r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { var body <%- dataType %> From a486ee0cb94415fe130b4d551d8167ed7dad1770 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 12 Apr 2024 10:33:11 +0700 Subject: [PATCH 268/346] chore: add examples of using query parameters Part of #23 --- examples/go/chi/mysql/routes.go | 25 ++++++++++++++++++++++ examples/js/express/mysql/endpoints.yaml | 18 ++++++++++++++++ examples/js/express/mysql/routes.js | 13 +++++++++++ examples/python/fastapi/postgres/routes.py | 22 +++++++++++++++++++ examples/ts/express/mysql/routes.ts | 13 +++++++++++ tests/crud.hurl | 14 ++++++++++++ 6 files changed, 105 insertions(+) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index edba032..fcbe0fc 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -110,6 +110,31 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { w.WriteHeader(http.StatusNoContent) }) + r.Get("/v1/categories/search", func(w http.ResponseWriter, r *http.Request) { + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + internalServerError(w) + return + } + + result := []CategoryDto{} + args := map[string]interface{}{ + "hidden": r.URL.Query().Get("hidden"), + } + err = stmt.Select(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) + internalServerError(w) + } + }) + r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId") if err != nil { diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 4f4b4c5..afd5c01 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -77,6 +77,24 @@ hidden: type: boolean +- path: /v1/categories/search + get_list: + query: >- + SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE hidden = :q.hidden + dto: + name: CategoryDto + fields: + id: + type: integer + hidden: + type: boolean + - path: /v1/categories/:categoryId get: query: >- diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index d856927..4943866 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -58,6 +58,19 @@ const register = (app, pool) => { ) }) + app.get('/v1/categories/search', (req, res, next) => { + pool.query( + 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', + { "hidden": req.query.hidden }, + (err, rows, fields) => { + if (err) { + return next(err) + } + res.json(rows) + } + ) + }) + app.get('/v1/categories/:categoryId', (req, res, next) => { pool.query( 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 01be31b..0cbdbc3 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -133,6 +133,28 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): conn.close() +@router.get('/v1/categories/search') +def get_list_v1_categories_search(hidden, conn=Depends(db_connection)): + try: + with conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + """ + SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE hidden = %(hidden)s + """, { + "hidden": hidden + }) + return cur.fetchall() + finally: + conn.close() + + @router.get('/v1/categories/{categoryId}') def get_v1_categories_category_id(categoryId, conn=Depends(db_connection)): try: diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index b3c6601..f730954 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -61,6 +61,19 @@ const register = (app: Express, pool: Pool) => { ) }) + app.get('/v1/categories/search', (req: Request, res: Response, next: NextFunction) => { + pool.query( + 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', + { "hidden": req.query.hidden }, + (err, rows, fields) => { + if (err) { + return next(err) + } + res.json(rows) + } + ) + }) + app.get('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', diff --git a/tests/crud.hurl b/tests/crud.hurl index 9de3b96..f50c68f 100644 --- a/tests/crud.hurl +++ b/tests/crud.hurl @@ -49,6 +49,13 @@ jsonpath "$[0].name_ru" == null jsonpath "$[0].slug" == "sport" jsonpath "$[0].hidden" == true +GET {{ SERVER_URL }} /v1/categories/search?hidden=true +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].name" == "Sport" +jsonpath "$[0].hidden" == true + # PUT should update an object PUT {{ SERVER_URL }}/v1/categories/1 @@ -71,6 +78,13 @@ jsonpath "$.name_ru" == "Фауна" jsonpath "$.slug" == "fauna" jsonpath "$.hidden" == false +GET {{ SERVER_URL }} /v1/categories/search?hidden=false +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].name" == "Fauna" +jsonpath "$[0].hidden" == false + # DELETE should remove an object DELETE {{ SERVER_URL }}/v1/categories/1 From 976aad4299e7cd786101f573d59d626c8ebe007c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 12 Apr 2024 10:41:59 +0700 Subject: [PATCH 269/346] chore: fix hurl syntax error Correction for a486ee0cb94415fe130b4d551d8167ed7dad1770 commit. Relate to #23 --- tests/crud.hurl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/crud.hurl b/tests/crud.hurl index f50c68f..ac18f20 100644 --- a/tests/crud.hurl +++ b/tests/crud.hurl @@ -49,7 +49,7 @@ jsonpath "$[0].name_ru" == null jsonpath "$[0].slug" == "sport" jsonpath "$[0].hidden" == true -GET {{ SERVER_URL }} /v1/categories/search?hidden=true +GET {{ SERVER_URL }}/v1/categories/search?hidden=true HTTP 200 [Asserts] jsonpath "$" count == 1 @@ -78,7 +78,7 @@ jsonpath "$.name_ru" == "Фауна" jsonpath "$.slug" == "fauna" jsonpath "$.hidden" == false -GET {{ SERVER_URL }} /v1/categories/search?hidden=false +GET {{ SERVER_URL }}/v1/categories/search?hidden=false HTTP 200 [Asserts] jsonpath "$" count == 1 From dd8e260f4d0b0ab05ba0f458ef061c1c969b4c37 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 13 Apr 2024 18:06:34 +0700 Subject: [PATCH 270/346] refactor(golang,python): extract method for getting field's type --- src/cli.js | 11 +++++++++++ src/templates/routes.go.ejs | 12 ++++-------- src/templates/routes.py.ejs | 13 ++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/cli.js b/src/cli.js index e3e01f0..f57ab40 100755 --- a/src/cli.js +++ b/src/cli.js @@ -157,6 +157,16 @@ const lengthOfLongestString = (arr) => arr 0 /* initial value */ ) +// returns user-defined variable's type or null +// Accepts method.dto.fields fieldsInfo +const retrieveType = (fieldsInfo, fieldName) => { + const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') + if (hasTypeInfo) { + return fieldsInfo[fieldName].type + } + return null +} + const createEndpoints = async (destDir, { lang }, config) => { const ext = lang2extension(lang) const fileName = `routes.${ext}` @@ -299,6 +309,7 @@ const createEndpoints = async (destDir, { lang }, config) => { "placeholdersMap": placeholdersMap, "removeComments": removeComments, + "retrieveType": retrieveType, } ) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 76a279a..b88a1dd 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -71,18 +71,14 @@ function extractProperties(queryAst) { } function findOutType(fieldsInfo, fieldName) { - const defaultType = '*string' - const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') - if (!hasTypeInfo) { - return defaultType - } - if (hasTypeInfo && fieldsInfo[fieldName].type === 'integer') { + const fieldType = retrieveType(fieldsInfo, fieldName) + if (fieldType === 'integer') { return '*int' } - if (hasTypeInfo && fieldsInfo[fieldName].type === 'boolean') { + if (fieldType === 'boolean') { return '*bool' } - return defaultType + return '*string' } function addTypes(props, fieldsInfo) { diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 74debe5..f3188b1 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -87,20 +87,15 @@ function extractProperties(queryAst) { return [] } -// LATER: try to reduce duplication with routes.go.ejs function findOutType(fieldsInfo, fieldName) { - const defaultType = 'str' - const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') - if (!hasTypeInfo) { - return defaultType - } - if (fieldsInfo[fieldName].type === 'integer') { + const fieldType = retrieveType(fieldsInfo, fieldName) + if (fieldType === 'integer') { return 'int' } - if (fieldsInfo[fieldName].type === 'boolean') { + if (fieldType === 'boolean') { return 'bool' } - return defaultType + return 'str' } // LATER: reduce duplication with routes.go.ejs From ea6df41c1a7387c1fe16fc075eef74dc13d72d2c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 13 Apr 2024 19:30:36 +0700 Subject: [PATCH 271/346] feat(js): add support for boolean type for query arguments Part of #49 --- examples/js/express/mysql/endpoints.yaml | 4 ++++ examples/js/express/mysql/routes.js | 6 +++++- src/cli.js | 8 ++++++-- src/templates/routes.js.ejs | 7 ++++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index afd5c01..7afd1f5 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -94,6 +94,10 @@ type: integer hidden: type: boolean + params: + query: + hidden: + type: boolean - path: /v1/categories/:categoryId get: diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 4943866..b8470fb 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -1,3 +1,7 @@ +const parseBoolean = (value) => { + return value === 'true' +} + const register = (app, pool) => { app.get('/v1/categories/count', (req, res, next) => { @@ -61,7 +65,7 @@ const register = (app, pool) => { app.get('/v1/categories/search', (req, res, next) => { pool.query( 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', - { "hidden": req.query.hidden }, + { "hidden": parseBoolean(req.query.hidden) }, (err, rows, fields) => { if (err) { return next(err) diff --git a/src/cli.js b/src/cli.js index f57ab40..d829a43 100755 --- a/src/cli.js +++ b/src/cli.js @@ -158,7 +158,7 @@ const lengthOfLongestString = (arr) => arr ) // returns user-defined variable's type or null -// Accepts method.dto.fields fieldsInfo +// Accepts method.dto.fields or method.params as fieldsInfo const retrieveType = (fieldsInfo, fieldName) => { const hasTypeInfo = fieldsInfo.hasOwnProperty(fieldName) && fieldsInfo[fieldName].hasOwnProperty('type') if (hasTypeInfo) { @@ -235,7 +235,7 @@ const createEndpoints = async (destDir, { lang }, config) => { // [ "p.page", "b.num" ] => '"page": req.params.page, "num": req.body.num' // (used only with Express) - "formatParamsAsJavaScriptObject": (params) => { + "formatParamsAsJavaScriptObject": (params, method) => { if (params.length === 0) { return params } @@ -245,6 +245,10 @@ const createEndpoints = async (destDir, { lang }, config) => { const bindTarget = p.substring(0, 1) const paramName = p.substring(2) const prefix = placeholdersMap['js'][bindTarget] + // LATER: add support for path (method.params.path) and body (method.dto.fields) parameters + if (method && bindTarget === 'q' && retrieveType(method.params.query, paramName) === 'boolean') { + return `"${paramName}": parseBoolean(${prefix}.${paramName})` + } return `"${paramName}": ${prefix}.${paramName}` } ).join(', ') diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 155867a..193ed77 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -1,3 +1,8 @@ +<%# LATER: add it only when there is at least one parameter of boolean type -%> +const parseBoolean = (value) => { + return value === 'true' +} + const register = (app, pool) => { <% endpoints.forEach(function(endpoint) { @@ -13,7 +18,7 @@ endpoints.forEach(function(endpoint) { const sql = formatQuery(method.query) const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 - ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' + ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' : '' if (hasGetOne || hasGetMany) { From e6cd4685e7d8cabc254896d8d7fd47f1167e97f7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 14 Apr 2024 07:44:02 +0700 Subject: [PATCH 272/346] feat(ts): add support for boolean type for query arguments Part of #49 --- examples/ts/express/mysql/routes.ts | 6 +++++- src/cli.js | 2 +- src/templates/routes.ts.ejs | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index f730954..73fb454 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -1,6 +1,10 @@ import { Express, NextFunction, Request, Response } from 'express' import { Pool } from 'mysql' +const parseBoolean = (value: any) => { + return typeof value === 'string' && value === 'true' +} + const register = (app: Express, pool: Pool) => { app.get('/v1/categories/count', (req: Request, res: Response, next: NextFunction) => { @@ -64,7 +68,7 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories/search', (req: Request, res: Response, next: NextFunction) => { pool.query( 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', - { "hidden": req.query.hidden }, + { "hidden": parseBoolean(req.query.hidden) }, (err, rows, fields) => { if (err) { return next(err) diff --git a/src/cli.js b/src/cli.js index d829a43..d33d1e9 100755 --- a/src/cli.js +++ b/src/cli.js @@ -246,7 +246,7 @@ const createEndpoints = async (destDir, { lang }, config) => { const paramName = p.substring(2) const prefix = placeholdersMap['js'][bindTarget] // LATER: add support for path (method.params.path) and body (method.dto.fields) parameters - if (method && bindTarget === 'q' && retrieveType(method.params.query, paramName) === 'boolean') { + if (bindTarget === 'q' && retrieveType(method.params.query, paramName) === 'boolean') { return `"${paramName}": parseBoolean(${prefix}.${paramName})` } return `"${paramName}": ${prefix}.${paramName}` diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index 86b96d8..52cd8de 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -1,6 +1,11 @@ import { Express, NextFunction, Request, Response } from 'express' import { Pool } from 'mysql' +<%# LATER: add it only when there is at least one parameter of boolean type -%> +const parseBoolean = (value: any) => { + return typeof value === 'string' && value === 'true' +} + const register = (app: Express, pool: Pool) => { <% endpoints.forEach(function(endpoint) { @@ -16,7 +21,7 @@ endpoints.forEach(function(endpoint) { const sql = formatQuery(method.query) const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 - ? '\n { ' + formatParamsAsJavaScriptObject(params) + ' },' + ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' : '' if (hasGetOne || hasGetMany) { From bb0fe9d270af39aa1d77792c156ad8f874058245 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 14 Apr 2024 08:29:41 +0700 Subject: [PATCH 273/346] feat(golang): add support for boolean type for query arguments Part of #49 --- examples/go/chi/mysql/routes.go | 11 ++++++++++- src/cli.js | 9 +++++++-- src/templates/routes.go.ejs | 19 +++++++++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index fcbe0fc..8d657a3 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -6,6 +6,7 @@ import "fmt" import "io" import "net/http" import "os" +import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" @@ -29,6 +30,14 @@ type CreateCategoryDto struct { UserId *int `json:"user_id" db:"user_id"` } +func parseBoolean(value string) bool { + boolValue, err := strconv.ParseBool(value) + if err != nil { + boolValue = false + } + return boolValue +} + func registerRoutes(r chi.Router, db *sqlx.DB) { r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { @@ -120,7 +129,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { result := []CategoryDto{} args := map[string]interface{}{ - "hidden": r.URL.Query().Get("hidden"), + "hidden": parseBoolean(r.URL.Query().Get("hidden")), } err = stmt.Select(&result, args) switch err { diff --git a/src/cli.js b/src/cli.js index d33d1e9..5203fb8 100755 --- a/src/cli.js +++ b/src/cli.js @@ -272,7 +272,7 @@ const createEndpoints = async (destDir, { lang }, config) => { // [ "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) => { + "formatParamsAsGolangMap": (params, method) => { if (params.length === 0) { return params } @@ -284,9 +284,14 @@ const createEndpoints = async (destDir, { lang }, config) => { const paramName = p.substring(2) const formatFunc = placeholdersMap['go'][bindTarget] const quotedParam = '"' + paramName + '":' + let extractParamExpr = formatFunc(paramName) + // LATER: add support for path (method.params.path) and body (method.dto.fields) parameters + if (bindTarget === 'q' && retrieveType(method.params.query, paramName) === 'boolean') { + extractParamExpr = `parseBoolean(${extractParamExpr})` + } // 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)},` + return `${quotedParam.padEnd(maxParamNameLength+1)} ${extractParamExpr},` } ).join('\n\t\t\t') }, diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index b88a1dd..853975a 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -6,6 +6,8 @@ import "fmt" import "io" import "net/http" import "os" +<%# LATER: add it only when there is at least one parameter of boolean type -%> +import "strconv" import "github.com/go-chi/chi" import "github.com/jmoiron/sqlx" @@ -175,6 +177,15 @@ endpoints.forEach(function(endpoint) { }) }) -%> +<%# LATER: add it only when there is at least one parameter of boolean type -%> +func parseBoolean(value string) bool { + boolValue, err := strconv.ParseBool(value) + if err != nil { + boolValue = false + } + return boolValue +} + func registerRoutes(r chi.Router, db *sqlx.DB) { <% endpoints.forEach(function(endpoint) { @@ -219,7 +230,7 @@ endpoints.forEach(function(endpoint) { <%- resultVariableDeclaration %> args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params) %> + <%- formatParamsAsGolangMap(params, method) %> } err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> @@ -246,7 +257,7 @@ endpoints.forEach(function(endpoint) { json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params) %> + <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( "<%- formatQuery(method.query) %>", @@ -269,7 +280,7 @@ endpoints.forEach(function(endpoint) { json.NewDecoder(r.Body).Decode(&body) args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params) %> + <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( "<%- formatQuery(method.query) %>", @@ -289,7 +300,7 @@ endpoints.forEach(function(endpoint) { %> r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params) %> + <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( "<%- formatQuery(method.query) %>", From 0fac891e3d6309e82ebb071639ee01da20a950f9 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 07:38:35 +0700 Subject: [PATCH 274/346] feat(js): keep formatting and indentation of the multiline SQL queries Part of #26 --- examples/js/express/mysql/routes.js | 64 +++++++++++++++++++++++++---- src/cli.js | 14 +++++++ src/templates/routes.js.ejs | 14 +++---- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index b8470fb..0faf01d 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -22,7 +22,11 @@ const register = (app, pool) => { app.get('/v1/collections/:collectionId/categories/count', (req, res, next) => { pool.query( - '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', + `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`, { "collectionId": req.params.collectionId }, (err, rows, fields) => { if (err) { @@ -39,7 +43,12 @@ const register = (app, pool) => { app.get('/v1/categories', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories`, (err, rows, fields) => { if (err) { return next(err) @@ -51,7 +60,27 @@ const register = (app, pool) => { app.post('/v1/categories', (req, res, next) => { pool.query( - 'INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )', + `INSERT + INTO categories + ( name + , name_ru + , slug + , hidden + , created_at + , created_by + , updated_at + , updated_by + ) + VALUES + ( :name + , :name_ru + , :slug + , :hidden + , NOW() + , :user_id + , NOW() + , :user_id + )`, { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { @@ -64,7 +93,13 @@ const register = (app, pool) => { app.get('/v1/categories/search', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE hidden = :hidden`, { "hidden": parseBoolean(req.query.hidden) }, (err, rows, fields) => { if (err) { @@ -77,7 +112,13 @@ const register = (app, pool) => { app.get('/v1/categories/:categoryId', (req, res, next) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE id = :categoryId`, { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -94,7 +135,14 @@ const register = (app, pool) => { app.put('/v1/categories/:categoryId', (req, res, next) => { pool.query( - 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + `UPDATE categories + SET name = :name + , name_ru = :name_ru + , slug = :slug + , hidden = :hidden + , updated_at = NOW() + , updated_by = :user_id + WHERE id = :categoryId`, { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -107,7 +155,9 @@ const register = (app, pool) => { app.delete('/v1/categories/:categoryId', (req, res, next) => { pool.query( - 'DELETE FROM categories WHERE id = :categoryId', + `DELETE + FROM categories + WHERE id = :categoryId`, { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { diff --git a/src/cli.js b/src/cli.js index 5203fb8..5a4b0d8 100755 --- a/src/cli.js +++ b/src/cli.js @@ -259,6 +259,20 @@ const createEndpoints = async (destDir, { lang }, config) => { return removePlaceholders(flattenQuery(removeComments(query))) }, + // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) + // and also use backticks for multiline strings + // (used only with JS) + "formatQueryForJs": (query, indentLevel) => { + const sql = removePlaceholders(removeComments(query)) + const indent = ' '.repeat(indentLevel) + const isMultilineSql = sql.indexOf('\n') >= 0 + if (isMultilineSql) { + const indentedSql = sql.replace(/\n/g, '\n' + indent) + return "\n" + indent + '`' + indentedSql + '`' + } + return `\n${indent}'${sql}'` + }, + // (used only with Golang) "convertPathPlaceholders": convertPathPlaceholders, "sqlParser": parser, diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 193ed77..60efe75 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -15,7 +15,7 @@ endpoints.forEach(function(endpoint) { } const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' - const sql = formatQuery(method.query) + const sql = formatQueryForJs(method.query, 12) const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' @@ -24,8 +24,7 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> app.get('<%- path %>', (req, res, next) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -47,8 +46,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'post') { %> app.post('<%- path %>', (req, res, next) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -62,8 +60,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'put') { %> app.put('<%- path %>', (req, res, next) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -77,8 +74,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'delete') { %> app.delete('<%- path %>', (req, res, next) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) From f408f121a7c177e1219938331778b3319e44d93e Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 07:41:13 +0700 Subject: [PATCH 275/346] feat(ts): keep formatting and indentation of the multiline SQL queries Part of #26 --- examples/ts/express/mysql/routes.ts | 64 +++++++++++++++++++++++++---- src/cli.js | 2 +- src/templates/routes.ts.ejs | 14 +++---- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 73fb454..21b7f1a 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -25,7 +25,11 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/collections/:collectionId/categories/count', (req: Request, res: Response, next: NextFunction) => { pool.query( - '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', + `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`, { "collectionId": req.params.collectionId }, (err, rows, fields) => { if (err) { @@ -42,7 +46,12 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories`, (err, rows, fields) => { if (err) { return next(err) @@ -54,7 +63,27 @@ const register = (app: Express, pool: Pool) => { app.post('/v1/categories', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )', + `INSERT + INTO categories + ( name + , name_ru + , slug + , hidden + , created_at + , created_by + , updated_at + , updated_by + ) + VALUES + ( :name + , :name_ru + , :slug + , :hidden + , NOW() + , :user_id + , NOW() + , :user_id + )`, { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, (err, rows, fields) => { if (err) { @@ -67,7 +96,13 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories/search', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE hidden = :hidden`, { "hidden": parseBoolean(req.query.hidden) }, (err, rows, fields) => { if (err) { @@ -80,7 +115,13 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId', + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE id = :categoryId`, { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -97,7 +138,14 @@ const register = (app: Express, pool: Pool) => { app.put('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId', + `UPDATE categories + SET name = :name + , name_ru = :name_ru + , slug = :slug + , hidden = :hidden + , updated_at = NOW() + , updated_by = :user_id + WHERE id = :categoryId`, { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { @@ -110,7 +158,9 @@ const register = (app: Express, pool: Pool) => { app.delete('/v1/categories/:categoryId', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'DELETE FROM categories WHERE id = :categoryId', + `DELETE + FROM categories + WHERE id = :categoryId`, { "categoryId": req.params.categoryId }, (err, rows, fields) => { if (err) { diff --git a/src/cli.js b/src/cli.js index 5a4b0d8..cc1eb63 100755 --- a/src/cli.js +++ b/src/cli.js @@ -261,7 +261,7 @@ const createEndpoints = async (destDir, { lang }, config) => { // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) // and also use backticks for multiline strings - // (used only with JS) + // (used only with JS, TS) "formatQueryForJs": (query, indentLevel) => { const sql = removePlaceholders(removeComments(query)) const indent = ' '.repeat(indentLevel) diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index 52cd8de..d0149f2 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -18,7 +18,7 @@ endpoints.forEach(function(endpoint) { } const hasGetOne = method.name === 'get' const hasGetMany = method.name === 'get_list' - const sql = formatQuery(method.query) + const sql = formatQueryForJs(method.query, 12) const params = extractParamsFromQuery(method.query) const formattedParams = params.length > 0 ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' @@ -27,8 +27,7 @@ endpoints.forEach(function(endpoint) { if (hasGetOne || hasGetMany) { %> app.get('<%- path %>', (req: Request, res: Response, next: NextFunction) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -50,8 +49,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'post') { %> app.post('<%- path %>', (req: Request, res: Response, next: NextFunction) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -65,8 +63,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'put') { %> app.put('<%- path %>', (req: Request, res: Response, next: NextFunction) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) @@ -80,8 +77,7 @@ endpoints.forEach(function(endpoint) { if (method.name === 'delete') { %> app.delete('<%- path %>', (req: Request, res: Response, next: NextFunction) => { - pool.query( - '<%- sql %>',<%- formattedParams %> + pool.query(<%- sql %>,<%- formattedParams %> (err, rows, fields) => { if (err) { return next(err) From 58211b668ff9a0c89c29cf52b5ef61645ca523c7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 07:50:16 +0700 Subject: [PATCH 276/346] refactor(python): move formatQueryForPython() function to cli.js --- src/cli.js | 14 ++++++++++++++ src/templates/routes.py.ejs | 13 ------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/cli.js b/src/cli.js index cc1eb63..902c46b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -273,6 +273,20 @@ const createEndpoints = async (destDir, { lang }, config) => { return `\n${indent}'${sql}'` }, + // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) + // and also use """ for multiline strings + // (used only with Python) + "formatQueryForPython": (query, indentLevel) => { + const sql = removePlaceholders(removeComments(query)) + const isMultilineSql = sql.indexOf('\n') >= 0 + if (isMultilineSql) { + const indent = ' '.repeat(indentLevel) + const indentedSql = sql.replace(/\n/g, '\n' + indent) + return `\n${indent}"""\n${indent}${indentedSql}\n${indent}"""` + } + return `"${sql}"` + }, + // (used only with Golang) "convertPathPlaceholders": convertPathPlaceholders, "sqlParser": parser, diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index f3188b1..7c19f7f 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -31,19 +31,6 @@ function convertToFastApiPath(path) { return path.replace(/:([_a-zA-Z]+)/g, '{$1}') } -// Differs from formatQuery() as it doesn't flatten query (preserve original formatting) -// and also use """ for multiline strings -function formatQueryForPython(query, indentLevel) { - const sql = removePlaceholders(removeComments(query)) - const isMultilineSql = sql.indexOf('\n') >= 0 - if (isMultilineSql) { - const indent = ' '.repeat(indentLevel) - const indentedSql = sql.replace(/\n/g, '\n' + indent) - return `\n${indent}"""\n${indent}${indentedSql}\n${indent}"""` - } - return `"${sql}"` -} - // LATER: reduce duplication with routes.go.ejs // {'values': // [ From 01591a38c58b1f1eeef5274d7e9aceb15aef53fa Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 07:54:37 +0700 Subject: [PATCH 277/346] refactor(golang): extract variable "sql" --- src/templates/routes.go.ejs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 853975a..c45101a 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -197,6 +197,8 @@ endpoints.forEach(function(endpoint) { return } + const sql = formatQuery(method.query) + // define before "if", to make it available later let dataType if (method.name !== 'delete') { @@ -221,7 +223,7 @@ endpoints.forEach(function(endpoint) { <% if (params.length > 0) { -%> - stmt, err := db.PrepareNamed("<%- formatQuery(method.query) %>") + stmt, err := db.PrepareNamed("<%- sql %>") if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -235,7 +237,7 @@ endpoints.forEach(function(endpoint) { err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> <%- resultVariableDeclaration %> - err := db.<%- queryFunction %>(&result, "<%- formatQuery(method.query) %>") + err := db.<%- queryFunction %>(&result, "<%- sql %>") <% } -%> switch err { case sql.ErrNoRows: @@ -260,7 +262,7 @@ endpoints.forEach(function(endpoint) { <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( - "<%- formatQuery(method.query) %>", + "<%- sql %>", args, ) if err != nil { @@ -283,7 +285,7 @@ endpoints.forEach(function(endpoint) { <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( - "<%- formatQuery(method.query) %>", + "<%- sql %>", args, ) if err != nil { @@ -303,7 +305,7 @@ endpoints.forEach(function(endpoint) { <%- formatParamsAsGolangMap(params, method) %> } _, err := db.NamedExec( - "<%- formatQuery(method.query) %>", + "<%- sql %>", args, ) if err != nil { From e817693104436dcb63491597a617477b5e384f7d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 08:05:49 +0700 Subject: [PATCH 278/346] refactor(golang): use tabs instead of spaces for indentation --- examples/go/chi/mysql/app.go | 82 +++--- examples/go/chi/mysql/routes.go | 338 ++++++++++++------------ src/templates/app.go.ejs | 82 +++--- src/templates/routes.go.ejs | 438 ++++++++++++++++---------------- 4 files changed, 470 insertions(+), 470 deletions(-) diff --git a/examples/go/chi/mysql/app.go b/examples/go/chi/mysql/app.go index dd4d946..cdf9d4f 100644 --- a/examples/go/chi/mysql/app.go +++ b/examples/go/chi/mysql/app.go @@ -9,45 +9,45 @@ import "github.com/jmoiron/sqlx" import _ "github.com/go-sql-driver/mysql" func main() { - mapper := func(name string) string { - value := os.Getenv(name) - switch name { - case "DB_HOST": - if value == "" { - value = "localhost" - } - case "DB_NAME", "DB_USER", "DB_PASSWORD": - if value == "" { - fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) - os.Exit(1) - } - } - return value - } - - dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) - db, err := sqlx.Open("mysql", dsn) - if err != nil { - fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) - os.Exit(1) - } - defer db.Close() - - if err = db.Ping(); err != nil { - fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) - os.Exit(1) - } - - r := chi.NewRouter() - registerRoutes(r, db) - - port := os.Getenv("PORT") - if port == "" { - port = "3000" - } - - fmt.Println("Listen on " + port) - err = http.ListenAndServe(":"+port, r) - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) - os.Exit(1) + mapper := func(name string) string { + value := os.Getenv(name) + switch name { + case "DB_HOST": + if value == "" { + value = "localhost" + } + case "DB_NAME", "DB_USER", "DB_PASSWORD": + if value == "" { + fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) + os.Exit(1) + } + } + return value + } + + dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) + db, err := sqlx.Open("mysql", dsn) + if err != nil { + fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) + os.Exit(1) + } + defer db.Close() + + if err = db.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) + os.Exit(1) + } + + r := chi.NewRouter() + registerRoutes(r, db) + + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + fmt.Println("Listen on " + port) + err = http.ListenAndServe(":"+port, r) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) + os.Exit(1) } diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 8d657a3..b15eb9c 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -31,190 +31,190 @@ type CreateCategoryDto struct { } func parseBoolean(value string) bool { - boolValue, err := strconv.ParseBool(value) - if err != nil { - boolValue = false - } - return boolValue + boolValue, err := strconv.ParseBool(value) + if err != nil { + boolValue = false + } + return boolValue } func registerRoutes(r chi.Router, db *sqlx.DB) { - r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { - var result CounterDto - err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories") - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - internalServerError(w) - } - }) - - r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { - stmt, 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") - if err != nil { - fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - internalServerError(w) - return - } - - var result CounterDto - args := map[string]interface{}{ - "collectionId": chi.URLParam(r, "collectionId"), - } - err = stmt.Get(&result, args) - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - internalServerError(w) - } - }) - - r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - result := []CategoryDto{} - err := db.Select(&result, "SELECT id , name , name_ru , slug , hidden FROM categories") - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) - internalServerError(w) - } - }) - - r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { - var body CreateCategoryDto - json.NewDecoder(r.Body).Decode(&body) - - args := map[string]interface{}{ - "name": body.Name, + r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { + var result CounterDto + err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories") + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + internalServerError(w) + } + }) + + r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { + stmt, 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") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + internalServerError(w) + return + } + + var result CounterDto + args := map[string]interface{}{ + "collectionId": chi.URLParam(r, "collectionId"), + } + err = stmt.Get(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + internalServerError(w) + } + }) + + r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { + result := []CategoryDto{} + err := db.Select(&result, "SELECT id , name , name_ru , slug , hidden FROM categories") + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) + internalServerError(w) + } + }) + + r.Post("/v1/categories", func(w http.ResponseWriter, r *http.Request) { + var body CreateCategoryDto + json.NewDecoder(r.Body).Decode(&body) + + args := map[string]interface{}{ + "name": body.Name, "name_ru": body.NameRu, "slug": body.Slug, "hidden": body.Hidden, "user_id": body.UserId, - } - _, err := db.NamedExec( - "INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) - - r.Get("/v1/categories/search", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden") - if err != nil { - fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - internalServerError(w) - return - } - - result := []CategoryDto{} - args := map[string]interface{}{ - "hidden": parseBoolean(r.URL.Query().Get("hidden")), - } - err = stmt.Select(&result, args) - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) - internalServerError(w) - } - }) - - r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId") - if err != nil { - fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - internalServerError(w) - return - } - - var result CategoryDto - args := map[string]interface{}{ - "categoryId": chi.URLParam(r, "categoryId"), - } - err = stmt.Get(&result, args) - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) - internalServerError(w) - } - }) - - r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - var body CreateCategoryDto - json.NewDecoder(r.Body).Decode(&body) - - args := map[string]interface{}{ - "name": body.Name, + } + _, err := db.NamedExec( + "INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + r.Get("/v1/categories/search", func(w http.ResponseWriter, r *http.Request) { + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + internalServerError(w) + return + } + + result := []CategoryDto{} + args := map[string]interface{}{ + "hidden": parseBoolean(r.URL.Query().Get("hidden")), + } + err = stmt.Select(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Select failed: %v\n", err) + internalServerError(w) + } + }) + + r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + internalServerError(w) + return + } + + var result CategoryDto + args := map[string]interface{}{ + "categoryId": chi.URLParam(r, "categoryId"), + } + err = stmt.Get(&result, args) + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "Get failed: %v\n", err) + internalServerError(w) + } + }) + + r.Put("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + var body CreateCategoryDto + json.NewDecoder(r.Body).Decode(&body) + + args := map[string]interface{}{ + "name": body.Name, "name_ru": body.NameRu, "slug": body.Slug, "hidden": body.Hidden, "user_id": body.UserId, "categoryId": chi.URLParam(r, "categoryId"), - } - _, err := db.NamedExec( - "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) - - r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - args := map[string]interface{}{ - "categoryId": chi.URLParam(r, "categoryId"), - } - _, err := db.NamedExec( - "DELETE FROM categories WHERE id = :categoryId", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) + } + _, err := db.NamedExec( + "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + r.Delete("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { + args := map[string]interface{}{ + "categoryId": chi.URLParam(r, "categoryId"), + } + _, err := db.NamedExec( + "DELETE FROM categories WHERE id = :categoryId", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) } func internalServerError(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, `{"error":"Internal Server Error"}`) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `{"error":"Internal Server Error"}`) } diff --git a/src/templates/app.go.ejs b/src/templates/app.go.ejs index dd4d946..cdf9d4f 100644 --- a/src/templates/app.go.ejs +++ b/src/templates/app.go.ejs @@ -9,45 +9,45 @@ import "github.com/jmoiron/sqlx" import _ "github.com/go-sql-driver/mysql" func main() { - mapper := func(name string) string { - value := os.Getenv(name) - switch name { - case "DB_HOST": - if value == "" { - value = "localhost" - } - case "DB_NAME", "DB_USER", "DB_PASSWORD": - if value == "" { - fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) - os.Exit(1) - } - } - return value - } - - dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) - db, err := sqlx.Open("mysql", dsn) - if err != nil { - fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) - os.Exit(1) - } - defer db.Close() - - if err = db.Ping(); err != nil { - fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) - os.Exit(1) - } - - r := chi.NewRouter() - registerRoutes(r, db) - - port := os.Getenv("PORT") - if port == "" { - port = "3000" - } - - fmt.Println("Listen on " + port) - err = http.ListenAndServe(":"+port, r) - fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) - os.Exit(1) + mapper := func(name string) string { + value := os.Getenv(name) + switch name { + case "DB_HOST": + if value == "" { + value = "localhost" + } + case "DB_NAME", "DB_USER", "DB_PASSWORD": + if value == "" { + fmt.Fprintf(os.Stderr, "%s env variable is not set or empty\n", name) + os.Exit(1) + } + } + return value + } + + dsn := os.Expand("${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/${DB_NAME}", mapper) + db, err := sqlx.Open("mysql", dsn) + if err != nil { + fmt.Fprintf(os.Stderr, "sqlx.Open failed: %v\n", err) + os.Exit(1) + } + defer db.Close() + + if err = db.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "Ping failed: could not connect to database: %v\n", err) + os.Exit(1) + } + + r := chi.NewRouter() + registerRoutes(r, db) + + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + fmt.Println("Listen on " + port) + err = http.ListenAndServe(":"+port, r) + fmt.Fprintf(os.Stderr, "ListenAndServe failed: %v\n", err) + os.Exit(1) } diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index c45101a..5c80fa6 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -21,8 +21,8 @@ import "github.com/jmoiron/sqlx" // ] // } => [ 'nameRu' ] function extractSelectParameters(queryAst) { - return queryAst.columns - .map(column => column.as !== null ? column.as : column.expr.column) + return queryAst.columns + .map(column => column.as !== null ? column.as : column.expr.column) } // {'values': @@ -34,10 +34,10 @@ function extractSelectParameters(queryAst) { // ] // } => [ 'user_id' ] function extractInsertValues(queryAst) { - const values = queryAst.values.flatMap(elem => elem.value) - .map(elem => elem.type === 'param' ? elem.value : null) - .filter(elem => elem) // filter out nulls - return Array.from(new Set(values)) + const values = queryAst.values.flatMap(elem => elem.value) + .map(elem => elem.type === 'param' ? elem.value : null) + .filter(elem => elem) // filter out nulls + return Array.from(new Set(values)) } // {'set': @@ -50,86 +50,86 @@ function extractInsertValues(queryAst) { // ] // } => [ 'user_id' ] function extractUpdateValues(queryAst) { - // LATER: distinguish between b.param and q.param and extract only first - return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null) - .filter(value => value) // filter out nulls + // LATER: distinguish between b.param and q.param and extract only first + return queryAst.set.map(elem => elem.value.type === 'param' ? elem.value.value : null) + .filter(value => value) // filter out nulls } // LATER: consider taking into account b.params from WHERE clause function extractProperties(queryAst) { - if (queryAst.type === 'select') { - return extractSelectParameters(queryAst) - } + if (queryAst.type === 'select') { + return extractSelectParameters(queryAst) + } - if (queryAst.type === 'insert') { - return extractInsertValues(queryAst) - } + if (queryAst.type === 'insert') { + return extractInsertValues(queryAst) + } - if (queryAst.type === 'update') { - return extractUpdateValues(queryAst) - } + if (queryAst.type === 'update') { + return extractUpdateValues(queryAst) + } - return [] + return [] } function findOutType(fieldsInfo, fieldName) { - const fieldType = retrieveType(fieldsInfo, fieldName) - if (fieldType === 'integer') { - return '*int' - } - if (fieldType === 'boolean') { - return '*bool' - } - return '*string' + const fieldType = retrieveType(fieldsInfo, fieldName) + if (fieldType === 'integer') { + return '*int' + } + if (fieldType === 'boolean') { + return '*bool' + } + return '*string' } function addTypes(props, fieldsInfo) { - return props.map(prop => { - return { - "name": prop, - "type": findOutType(fieldsInfo, prop), - } - }) + return props.map(prop => { + return { + "name": prop, + "type": findOutType(fieldsInfo, prop), + } + }) } function query2dto(parser, method) { - const query = removePlaceholders(method.query) - const queryAst = parser.astify(query) - const props = extractProperties(queryAst) - if (props.length === 0) { - console.warn('Could not create DTO for query:', formatQuery(query)) - console.debug('Query AST:') - console.debug(queryAst) - return null - } - const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {} - const propsWithTypes = addTypes(props, fieldsInfo) - const hasName = method.dto && method.dto.name && method.dto.name.length > 0 - const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter - return { - "name": name, - "hasUserProvidedName": hasName, - "props": propsWithTypes, - // max lengths are needed for proper formatting - "maxFieldNameLength": lengthOfLongestString(props.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))), - "maxFieldTypeLength": lengthOfLongestString(propsWithTypes.map(el => el.type)), - // required for de-duplication - // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" - // LATER: sort before join - "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') - } + const query = removePlaceholders(method.query) + const queryAst = parser.astify(query) + const props = extractProperties(queryAst) + if (props.length === 0) { + console.warn('Could not create DTO for query:', formatQuery(query)) + console.debug('Query AST:') + console.debug(queryAst) + return null + } + const fieldsInfo = method.dto && method.dto.fields ? method.dto.fields : {} + const propsWithTypes = addTypes(props, fieldsInfo) + const hasName = method.dto && method.dto.name && method.dto.name.length > 0 + const name = hasName ? method.dto.name : "Dto" + ++globalDtoCounter + return { + "name": name, + "hasUserProvidedName": hasName, + "props": propsWithTypes, + // max lengths are needed for proper formatting + "maxFieldNameLength": lengthOfLongestString(props.map(el => el.indexOf('_') < 0 ? el : el.replace(/_/g, ''))), + "maxFieldTypeLength": lengthOfLongestString(propsWithTypes.map(el => el.type)), + // required for de-duplication + // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string" + // LATER: sort before join + "signature": propsWithTypes.map(field => `${field.name}=${field.type}`).join(' ') + } } function dto2struct(dto) { - let result = `type ${dto.name} struct {\n` - dto.props.forEach(prop => { - const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength) - const fieldType = prop.type.padEnd(dto.maxFieldTypeLength) - result += `\t${fieldName} ${fieldType} \`json:"${prop.name}" db:"${prop.name}"\`\n` - }) - result += '}\n' - - return result + let result = `type ${dto.name} struct {\n` + dto.props.forEach(prop => { + const fieldName = capitalize(snake2camelCase(prop.name)).padEnd(dto.maxFieldNameLength) + const fieldType = prop.type.padEnd(dto.maxFieldTypeLength) + result += `\t${fieldName} ${fieldType} \`json:"${prop.name}" db:"${prop.name}"\`\n` + }) + result += '}\n' + + return result } let globalDtoCounter = 0 @@ -138,187 +138,187 @@ const dtoCache = {} const namedDtoCache = {} function cacheDto(dto) { - if (dto.hasUserProvidedName) { - namedDtoCache[dto.signature] = dto.name - } else { - dtoCache[dto.signature] = dto.name - } - return dto + if (dto.hasUserProvidedName) { + namedDtoCache[dto.signature] = dto.name + } else { + dtoCache[dto.signature] = dto.name + } + return dto } function dtoInCache(dto) { - const existsNamed = namedDtoCache.hasOwnProperty(dto.signature) - // always prefer user specified name even when we have a similar DTO in cache for generated names - if (dto.hasUserProvidedName) { - return existsNamed - } - // prefer to re-use named DTO - return existsNamed || dtoCache.hasOwnProperty(dto.signature) + const existsNamed = namedDtoCache.hasOwnProperty(dto.signature) + // always prefer user specified name even when we have a similar DTO in cache for generated names + if (dto.hasUserProvidedName) { + return existsNamed + } + // prefer to re-use named DTO + return existsNamed || dtoCache.hasOwnProperty(dto.signature) } function obtainDtoName(dto) { - const cacheKey = dto.signature - return namedDtoCache.hasOwnProperty(cacheKey) ? namedDtoCache[cacheKey] : dto.name + const cacheKey = dto.signature + return namedDtoCache.hasOwnProperty(cacheKey) ? namedDtoCache[cacheKey] : dto.name } const verbs_with_dto = [ 'get', 'post', 'put' ] endpoints.forEach(function(endpoint) { - const dtos = endpoint.methods - .filter(method => method.query) // filter out aggregated_queries for a while (see #17) - .filter(method => verbs_with_dto.includes(method.verb)) - .map(method => query2dto(sqlParser, method)) - .filter(elem => elem) // filter out nulls - .filter(dto => !dtoInCache(dto)) - .map(dto => dto2struct(cacheDto(dto))) - .forEach(struct => { + const dtos = endpoint.methods + .filter(method => method.query) // filter out aggregated_queries for a while (see #17) + .filter(method => verbs_with_dto.includes(method.verb)) + .map(method => query2dto(sqlParser, method)) + .filter(elem => elem) // filter out nulls + .filter(dto => !dtoInCache(dto)) + .map(dto => dto2struct(cacheDto(dto))) + .forEach(struct => { -%> <%- struct %> <% - }) + }) }) -%> <%# LATER: add it only when there is at least one parameter of boolean type -%> func parseBoolean(value string) bool { - boolValue, err := strconv.ParseBool(value) - if err != nil { - boolValue = false - } - return boolValue + boolValue, err := strconv.ParseBool(value) + if err != nil { + boolValue = false + } + return boolValue } func registerRoutes(r chi.Router, db *sqlx.DB) { <% endpoints.forEach(function(endpoint) { - const path = convertPathPlaceholders(endpoint.path) - - endpoint.methods.forEach(function(method) { - if (!method.query) { - // filter out aggregated_queries for a while (see #17) - return - } - - const sql = formatQuery(method.query) - - // define before "if", to make it available later - let dataType - if (method.name !== 'delete') { - const dto = query2dto(sqlParser, method) - // LATER: do we really need signature and cache? - dataType = obtainDtoName(dto) - } - - const params = extractParamsFromQuery(method.query) - const hasGetOne = method.name === 'get' - const hasGetMany = method.name === 'get_list' - if (hasGetOne || hasGetMany) { - const resultVariableDeclaration = hasGetMany - ? `result := []${dataType}\{\}` - : `var result ${dataType}` - - const queryFunction = hasGetOne ? 'Get' : 'Select' - // LATER: handle only particular method (get/post/put) - // LATER: include method/path into an error message + const path = convertPathPlaceholders(endpoint.path) + + endpoint.methods.forEach(function(method) { + if (!method.query) { + // filter out aggregated_queries for a while (see #17) + return + } + + const sql = formatQuery(method.query) + + // define before "if", to make it available later + let dataType + if (method.name !== 'delete') { + const dto = query2dto(sqlParser, method) + // LATER: do we really need signature and cache? + dataType = obtainDtoName(dto) + } + + const params = extractParamsFromQuery(method.query) + const hasGetOne = method.name === 'get' + const hasGetMany = method.name === 'get_list' + if (hasGetOne || hasGetMany) { + const resultVariableDeclaration = hasGetMany + ? `result := []${dataType}\{\}` + : `var result ${dataType}` + + const queryFunction = hasGetOne ? 'Get' : 'Select' + // LATER: handle only particular method (get/post/put) + // LATER: include method/path into an error message %> - r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + r.Get("<%- path %>", func(w http.ResponseWriter, r *http.Request) { <% - if (params.length > 0) { + if (params.length > 0) { -%> - stmt, err := db.PrepareNamed("<%- sql %>") - if err != nil { - fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) - internalServerError(w) - return - } - - <%- resultVariableDeclaration %> - args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params, method) %> - } - err = stmt.<%- queryFunction %>(&result, args) -<% } else { -%> - <%- resultVariableDeclaration %> - err := db.<%- queryFunction %>(&result, "<%- sql %>") -<% } -%> - switch err { - case sql.ErrNoRows: - w.WriteHeader(http.StatusNotFound) - case nil: - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&result) - default: - fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err) - internalServerError(w) - } - }) + stmt, err := db.PrepareNamed("<%- sql %>") + if err != nil { + fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) + internalServerError(w) + return + } + + <%- resultVariableDeclaration %> + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params, method) %> + } + err = stmt.<%- queryFunction %>(&result, args) +<% } else { -%> + <%- resultVariableDeclaration %> + err := db.<%- queryFunction %>(&result, "<%- sql %>") +<% } -%> + switch err { + case sql.ErrNoRows: + w.WriteHeader(http.StatusNotFound) + case nil: + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + default: + fmt.Fprintf(os.Stderr, "<%- queryFunction %> failed: %v\n", err) + internalServerError(w) + } + }) <% - } - if (method.name === 'post') { + } + if (method.name === 'post') { %> - r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var body <%- dataType %> - json.NewDecoder(r.Body).Decode(&body) - - args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params, method) %> - } - _, err := db.NamedExec( - "<%- sql %>", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) + r.Post("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + var body <%- dataType %> + json.NewDecoder(r.Body).Decode(&body) + + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params, method) %> + } + _, err := db.NamedExec( + "<%- sql %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) <% - } - if (method.name === 'put') { + } + if (method.name === 'put') { %> - r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - var body <%- dataType %> - json.NewDecoder(r.Body).Decode(&body) - - args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params, method) %> - } - _, err := db.NamedExec( - "<%- sql %>", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) + r.Put("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + var body <%- dataType %> + json.NewDecoder(r.Body).Decode(&body) + + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params, method) %> + } + _, err := db.NamedExec( + "<%- sql %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) <% - } - if (method.name === 'delete') { + } + if (method.name === 'delete') { %> - r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { - args := map[string]interface{}{ - <%- formatParamsAsGolangMap(params, method) %> - } - _, err := db.NamedExec( - "<%- sql %>", - args, - ) - if err != nil { - fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) - internalServerError(w) - return - } - - w.WriteHeader(http.StatusNoContent) - }) + r.Delete("<%- path %>", func(w http.ResponseWriter, r *http.Request) { + args := map[string]interface{}{ + <%- formatParamsAsGolangMap(params, method) %> + } + _, err := db.NamedExec( + "<%- sql %>", + args, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "NamedExec failed: %v\n", err) + internalServerError(w) + return + } + + w.WriteHeader(http.StatusNoContent) + }) <% - } - }) + } + }) }) %> } @@ -326,7 +326,7 @@ endpoints.forEach(function(endpoint) { <%# IMPORTANT: WriteHeader() must be called after w.Header() -%> <%# w.Write() vs io.WriteString(): https://stackoverflow.com/questions/37863374/whats-the-difference-between-responsewriter-write-and-io-writestring -%> func internalServerError(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, `{"error":"Internal Server Error"}`) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `{"error":"Internal Server Error"}`) } From a9ff2802c395e2fa6e0289fa2dea3f97a895b2a6 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 10:35:04 +0700 Subject: [PATCH 279/346] refactor(js,ts): use double quotes for a single-line SQL queries Relate to #26 --- examples/js/express/mysql/routes.js | 2 +- examples/ts/express/mysql/routes.ts | 2 +- src/cli.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 0faf01d..305668e 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -6,7 +6,7 @@ const register = (app, pool) => { app.get('/v1/categories/count', (req, res, next) => { pool.query( - 'SELECT COUNT(*) AS counter FROM categories', + "SELECT COUNT(*) AS counter FROM categories", (err, rows, fields) => { if (err) { return next(err) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 21b7f1a..84f7eab 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -9,7 +9,7 @@ const register = (app: Express, pool: Pool) => { app.get('/v1/categories/count', (req: Request, res: Response, next: NextFunction) => { pool.query( - 'SELECT COUNT(*) AS counter FROM categories', + "SELECT COUNT(*) AS counter FROM categories", (err, rows, fields) => { if (err) { return next(err) diff --git a/src/cli.js b/src/cli.js index 902c46b..917be65 100755 --- a/src/cli.js +++ b/src/cli.js @@ -270,7 +270,7 @@ const createEndpoints = async (destDir, { lang }, config) => { const indentedSql = sql.replace(/\n/g, '\n' + indent) return "\n" + indent + '`' + indentedSql + '`' } - return `\n${indent}'${sql}'` + return `\n${indent}"${sql}"` }, // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) From 06a2f0c536c72810e7f35cac37383ef92d04dc96 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 10:32:56 +0700 Subject: [PATCH 280/346] feat(golang): keep formatting and indentation of the multiline SQL queries Part of #26 --- examples/go/chi/mysql/routes.go | 73 +++++++++++++++++++++++++++++---- src/cli.js | 2 +- src/templates/routes.go.ejs | 16 ++++---- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index b15eb9c..4bf7012 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -42,7 +42,9 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { r.Get("/v1/categories/count", func(w http.ResponseWriter, r *http.Request) { var result CounterDto - err := db.Get(&result, "SELECT COUNT(*) AS counter FROM categories") + err := db.Get( + &result, + "SELECT COUNT(*) AS counter FROM categories") switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) @@ -56,7 +58,12 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/collections/{collectionId}/categories/count", func(w http.ResponseWriter, r *http.Request) { - stmt, 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") + stmt, 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`) if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -82,7 +89,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { r.Get("/v1/categories", func(w http.ResponseWriter, r *http.Request) { result := []CategoryDto{} - err := db.Select(&result, "SELECT id , name , name_ru , slug , hidden FROM categories") + err := db.Select( + &result, + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories`) switch err { case sql.ErrNoRows: w.WriteHeader(http.StatusNotFound) @@ -107,7 +121,27 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { "user_id": body.UserId, } _, err := db.NamedExec( - "INSERT INTO categories ( name , name_ru , slug , hidden , created_at , created_by , updated_at , updated_by ) VALUES ( :name , :name_ru , :slug , :hidden , NOW() , :user_id , NOW() , :user_id )", + `INSERT + INTO categories + ( name + , name_ru + , slug + , hidden + , created_at + , created_by + , updated_at + , updated_by + ) + VALUES + ( :name + , :name_ru + , :slug + , :hidden + , NOW() + , :user_id + , NOW() + , :user_id + )`, args, ) if err != nil { @@ -120,7 +154,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories/search", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE hidden = :hidden") + stmt, err := db.PrepareNamed( + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE hidden = :hidden`) if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -145,7 +186,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { }) r.Get("/v1/categories/{categoryId}", func(w http.ResponseWriter, r *http.Request) { - stmt, err := db.PrepareNamed("SELECT id , name , name_ru , slug , hidden FROM categories WHERE id = :categoryId") + stmt, err := db.PrepareNamed( + `SELECT id + , name + , name_ru + , slug + , hidden + FROM categories + WHERE id = :categoryId`) if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -182,7 +230,14 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { "categoryId": chi.URLParam(r, "categoryId"), } _, err := db.NamedExec( - "UPDATE categories SET name = :name , name_ru = :name_ru , slug = :slug , hidden = :hidden , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId", + `UPDATE categories + SET name = :name + , name_ru = :name_ru + , slug = :slug + , hidden = :hidden + , updated_at = NOW() + , updated_by = :user_id + WHERE id = :categoryId`, args, ) if err != nil { @@ -199,7 +254,9 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { "categoryId": chi.URLParam(r, "categoryId"), } _, err := db.NamedExec( - "DELETE FROM categories WHERE id = :categoryId", + `DELETE + FROM categories + WHERE id = :categoryId`, args, ) if err != nil { diff --git a/src/cli.js b/src/cli.js index 917be65..6af0fc7 100755 --- a/src/cli.js +++ b/src/cli.js @@ -261,7 +261,7 @@ const createEndpoints = async (destDir, { lang }, config) => { // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) // and also use backticks for multiline strings - // (used only with JS, TS) + // (used only with JS, TS, Golang) "formatQueryForJs": (query, indentLevel) => { const sql = removePlaceholders(removeComments(query)) const indent = ' '.repeat(indentLevel) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 5c80fa6..09b6aca 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -197,7 +197,7 @@ endpoints.forEach(function(endpoint) { return } - const sql = formatQuery(method.query) + const sql = formatQueryForJs(method.query, 12) // define before "if", to make it available later let dataType @@ -223,7 +223,7 @@ endpoints.forEach(function(endpoint) { <% if (params.length > 0) { -%> - stmt, err := db.PrepareNamed("<%- sql %>") + stmt, err := db.PrepareNamed(<%- sql %>) if err != nil { fmt.Fprintf(os.Stderr, "PrepareNamed failed: %v\n", err) internalServerError(w) @@ -237,7 +237,8 @@ endpoints.forEach(function(endpoint) { err = stmt.<%- queryFunction %>(&result, args) <% } else { -%> <%- resultVariableDeclaration %> - err := db.<%- queryFunction %>(&result, "<%- sql %>") + err := db.<%- queryFunction %>( + &result,<%- sql %>) <% } -%> switch err { case sql.ErrNoRows: @@ -261,8 +262,7 @@ endpoints.forEach(function(endpoint) { args := map[string]interface{}{ <%- formatParamsAsGolangMap(params, method) %> } - _, err := db.NamedExec( - "<%- sql %>", + _, err := db.NamedExec(<%- sql %>, args, ) if err != nil { @@ -284,8 +284,7 @@ endpoints.forEach(function(endpoint) { args := map[string]interface{}{ <%- formatParamsAsGolangMap(params, method) %> } - _, err := db.NamedExec( - "<%- sql %>", + _, err := db.NamedExec(<%- sql %>, args, ) if err != nil { @@ -304,8 +303,7 @@ endpoints.forEach(function(endpoint) { args := map[string]interface{}{ <%- formatParamsAsGolangMap(params, method) %> } - _, err := db.NamedExec( - "<%- sql %>", + _, err := db.NamedExec(<%- sql %>, args, ) if err != nil { From bb2f8cce367329fa5f347e46a7df1dd1ab7baff0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 10:38:30 +0700 Subject: [PATCH 281/346] refactor: rename formatQuery() to formatQueryAsSingleLine() --- src/cli.js | 8 +++----- src/templates/routes.go.ejs | 2 +- src/templates/routes.py.ejs | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/cli.js b/src/cli.js index 6af0fc7..e6b9032 100755 --- a/src/cli.js +++ b/src/cli.js @@ -255,12 +255,11 @@ const createEndpoints = async (destDir, { lang }, config) => { }, // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" - "formatQuery": (query) => { + "formatQueryAsSingleLine": (query) => { return removePlaceholders(flattenQuery(removeComments(query))) }, - // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) - // and also use backticks for multiline strings + // Uses backticks for multiline strings. // (used only with JS, TS, Golang) "formatQueryForJs": (query, indentLevel) => { const sql = removePlaceholders(removeComments(query)) @@ -273,8 +272,7 @@ const createEndpoints = async (destDir, { lang }, config) => { return `\n${indent}"${sql}"` }, - // Differs from formatQuery() as it doesn't flatten query (preserve original formatting) - // and also use """ for multiline strings + // Uses """ for multiline strings. // (used only with Python) "formatQueryForPython": (query, indentLevel) => { const sql = removePlaceholders(removeComments(query)) diff --git a/src/templates/routes.go.ejs b/src/templates/routes.go.ejs index 09b6aca..cd33e9b 100644 --- a/src/templates/routes.go.ejs +++ b/src/templates/routes.go.ejs @@ -97,7 +97,7 @@ function query2dto(parser, method) { const queryAst = parser.astify(query) const props = extractProperties(queryAst) if (props.length === 0) { - console.warn('Could not create DTO for query:', formatQuery(query)) + console.warn('Could not create DTO for query:', formatQueryAsSingleLine(query)) console.debug('Query AST:') console.debug(queryAst) return null diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 7c19f7f..2f23cdd 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -101,7 +101,7 @@ function query2dto(parser, method) { const queryAst = parser.astify(query) const props = extractProperties(queryAst) if (props.length === 0) { - console.warn('Could not create DTO for query:', formatQuery(query)) + console.warn('Could not create DTO for query:', formatQueryAsSingleLine(query)) console.debug('Query AST:') console.debug(queryAst) return null From 81cce997419034071e940785ae1072b0cd0ae0f7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 10:52:54 +0700 Subject: [PATCH 282/346] refactor(js,ts): move formatting logic into formatParamsAsJavaScriptObject() --- src/cli.js | 6 ++++-- src/templates/routes.js.ejs | 4 +--- src/templates/routes.ts.ejs | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/cli.js b/src/cli.js index e6b9032..9099d6d 100755 --- a/src/cli.js +++ b/src/cli.js @@ -239,7 +239,9 @@ const createEndpoints = async (destDir, { lang }, config) => { if (params.length === 0) { return params } - return Array.from( + const indentLevel = 12 + const indent = ' '.repeat(indentLevel) + return `\n${indent}{ ` + Array.from( new Set(params), p => { const bindTarget = p.substring(0, 1) @@ -251,7 +253,7 @@ const createEndpoints = async (destDir, { lang }, config) => { } return `"${paramName}": ${prefix}.${paramName}` } - ).join(', ') + ).join(', ') + ' },' }, // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index 60efe75..faa3266 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -17,9 +17,7 @@ endpoints.forEach(function(endpoint) { const hasGetMany = method.name === 'get_list' const sql = formatQueryForJs(method.query, 12) const params = extractParamsFromQuery(method.query) - const formattedParams = params.length > 0 - ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' - : '' + const formattedParams = formatParamsAsJavaScriptObject(params, method) if (hasGetOne || hasGetMany) { %> diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index d0149f2..7319f4f 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -20,9 +20,7 @@ endpoints.forEach(function(endpoint) { const hasGetMany = method.name === 'get_list' const sql = formatQueryForJs(method.query, 12) const params = extractParamsFromQuery(method.query) - const formattedParams = params.length > 0 - ? '\n { ' + formatParamsAsJavaScriptObject(params, method) + ' },' - : '' + const formattedParams = formatParamsAsJavaScriptObject(params, method) if (hasGetOne || hasGetMany) { %> From 39558730230e8652d5a277cb9605fac4f82de258 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 16 Apr 2024 10:59:38 +0700 Subject: [PATCH 283/346] refactor(js,ts): format query parameters on multiple lines --- examples/js/express/mysql/routes.js | 33 +++++++++++++++++++++++------ examples/ts/express/mysql/routes.ts | 33 +++++++++++++++++++++++------ src/cli.js | 14 ++++++------ 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 305668e..7a055b4 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -27,7 +27,9 @@ const register = (app, pool) => { JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :collectionId`, - { "collectionId": req.params.collectionId }, + { + "collectionId": req.params.collectionId + }, (err, rows, fields) => { if (err) { return next(err) @@ -81,7 +83,13 @@ const register = (app, pool) => { , NOW() , :user_id )`, - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, + { + "name": req.body.name, + "name_ru": req.body.name_ru, + "slug": req.body.slug, + "hidden": req.body.hidden, + "user_id": req.body.user_id + }, (err, rows, fields) => { if (err) { return next(err) @@ -100,7 +108,9 @@ const register = (app, pool) => { , hidden FROM categories WHERE hidden = :hidden`, - { "hidden": parseBoolean(req.query.hidden) }, + { + "hidden": parseBoolean(req.query.hidden) + }, (err, rows, fields) => { if (err) { return next(err) @@ -119,7 +129,9 @@ const register = (app, pool) => { , hidden FROM categories WHERE id = :categoryId`, - { "categoryId": req.params.categoryId }, + { + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) @@ -143,7 +155,14 @@ const register = (app, pool) => { , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId`, - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + { + "name": req.body.name, + "name_ru": req.body.name_ru, + "slug": req.body.slug, + "hidden": req.body.hidden, + "user_id": req.body.user_id, + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) @@ -158,7 +177,9 @@ const register = (app, pool) => { `DELETE FROM categories WHERE id = :categoryId`, - { "categoryId": req.params.categoryId }, + { + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 84f7eab..9705484 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -30,7 +30,9 @@ const register = (app: Express, pool: Pool) => { JOIN series s ON s.id = cs.series_id WHERE cs.collection_id = :collectionId`, - { "collectionId": req.params.collectionId }, + { + "collectionId": req.params.collectionId + }, (err, rows, fields) => { if (err) { return next(err) @@ -84,7 +86,13 @@ const register = (app: Express, pool: Pool) => { , NOW() , :user_id )`, - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id }, + { + "name": req.body.name, + "name_ru": req.body.name_ru, + "slug": req.body.slug, + "hidden": req.body.hidden, + "user_id": req.body.user_id + }, (err, rows, fields) => { if (err) { return next(err) @@ -103,7 +111,9 @@ const register = (app: Express, pool: Pool) => { , hidden FROM categories WHERE hidden = :hidden`, - { "hidden": parseBoolean(req.query.hidden) }, + { + "hidden": parseBoolean(req.query.hidden) + }, (err, rows, fields) => { if (err) { return next(err) @@ -122,7 +132,9 @@ const register = (app: Express, pool: Pool) => { , hidden FROM categories WHERE id = :categoryId`, - { "categoryId": req.params.categoryId }, + { + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) @@ -146,7 +158,14 @@ const register = (app: Express, pool: Pool) => { , updated_at = NOW() , updated_by = :user_id WHERE id = :categoryId`, - { "name": req.body.name, "name_ru": req.body.name_ru, "slug": req.body.slug, "hidden": req.body.hidden, "user_id": req.body.user_id, "categoryId": req.params.categoryId }, + { + "name": req.body.name, + "name_ru": req.body.name_ru, + "slug": req.body.slug, + "hidden": req.body.hidden, + "user_id": req.body.user_id, + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) @@ -161,7 +180,9 @@ const register = (app: Express, pool: Pool) => { `DELETE FROM categories WHERE id = :categoryId`, - { "categoryId": req.params.categoryId }, + { + "categoryId": req.params.categoryId + }, (err, rows, fields) => { if (err) { return next(err) diff --git a/src/cli.js b/src/cli.js index 9099d6d..c4e0e08 100755 --- a/src/cli.js +++ b/src/cli.js @@ -239,9 +239,11 @@ const createEndpoints = async (destDir, { lang }, config) => { if (params.length === 0) { return params } - const indentLevel = 12 - const indent = ' '.repeat(indentLevel) - return `\n${indent}{ ` + Array.from( + const initialIndentLevel = 12 + const codeIndentLevel = initialIndentLevel + 4 + const initialIndent = ' '.repeat(initialIndentLevel) + const indent = ' '.repeat(codeIndentLevel) + return `\n${initialIndent}{\n` + Array.from( new Set(params), p => { const bindTarget = p.substring(0, 1) @@ -249,11 +251,11 @@ const createEndpoints = async (destDir, { lang }, config) => { const prefix = placeholdersMap['js'][bindTarget] // LATER: add support for path (method.params.path) and body (method.dto.fields) parameters if (bindTarget === 'q' && retrieveType(method.params.query, paramName) === 'boolean') { - return `"${paramName}": parseBoolean(${prefix}.${paramName})` + return `${indent}"${paramName}": parseBoolean(${prefix}.${paramName})` } - return `"${paramName}": ${prefix}.${paramName}` + return `${indent}"${paramName}": ${prefix}.${paramName}` } - ).join(', ') + ' },' + ).join(',\n') + `\n${initialIndent}},` }, // "SELECT *\n FROM foo WHERE id = :p.id" => "SELECT * FROM foo WHERE id = :id" From a5de675ef8c0aaa82c8308125ad28dad11777286 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 07:47:44 +0700 Subject: [PATCH 284/346] test: add integration test for custom routes Part of #27 --- .github/workflows/integration-tests.yml | 3 ++- examples/python/fastapi/postgres/custom_routes.py | 6 +++--- tests/misc.hurl | 13 +++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 tests/misc.hurl diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a8c288b..32b3712 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -91,7 +91,8 @@ jobs: --error-format long \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --test \ - tests/crud.hurl + tests/crud.hurl \ + tests/misc.hurl - name: Show application logs if: failure() diff --git a/examples/python/fastapi/postgres/custom_routes.py b/examples/python/fastapi/postgres/custom_routes.py index 80b85a3..5bf9232 100644 --- a/examples/python/fastapi/postgres/custom_routes.py +++ b/examples/python/fastapi/postgres/custom_routes.py @@ -3,6 +3,6 @@ router = APIRouter() -@router.get('/v1/hello') -def greetings(): - return {"hello": "world!"} +@router.get('/custom/route') +def customRoute(): + return { "custom": True } diff --git a/tests/misc.hurl b/tests/misc.hurl new file mode 100644 index 0000000..bb4e330 --- /dev/null +++ b/tests/misc.hurl @@ -0,0 +1,13 @@ +# +# Tests for various operations +# +# How to run: +# hurl --variable SERVER_URL=http://127.0.0.1:3000 misc.hurl --test +# + + +# Custom route +GET {{ SERVER_URL }}/custom/route +HTTP 200 +[Asserts] +jsonpath "$.custom" == true From 71f12f3309dc7825fb6f1dc51c424be129d6ef79 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 07:49:51 +0700 Subject: [PATCH 285/346] chore: remove a comment Should be in a5de675ef8c0aaa82c8308125ad28dad11777286 commit. Relate to #27 --- src/cli.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli.js b/src/cli.js index c4e0e08..a702734 100755 --- a/src/cli.js +++ b/src/cli.js @@ -104,7 +104,6 @@ const createApp = async (destDir, { lang }) => { `${__dirname}/templates/${fileName}.ejs`, { // @todo #27 Document usage of user defined routes - // @todo #27 Add integration test to ensure that custom router is picked up 'customRouteFilenames': customRouters } ) From e4c4814ecfc4ffa20623be4eeccfaea33dfcb764 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 08:26:09 +0700 Subject: [PATCH 286/346] feat(js): include user defined routes from *_routes.js files In order to include custom routes: - create a file _routes.js - export a function "register" that accepts 2 arguments (express router and MySQL connection pool) Part of #27 --- examples/js/express/mysql/app.js | 3 +++ examples/js/express/mysql/custom_routes.js | 7 +++++++ src/templates/app.js.ejs | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 examples/js/express/mysql/custom_routes.js diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index bd5a057..8f2be94 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -1,6 +1,7 @@ const express = require('express') const mysql = require('mysql') const routes = require('./routes') +const custom_routes = require('./custom_routes') const app = express() app.use(express.json()) @@ -34,6 +35,8 @@ const pool = mysql.createPool({ routes.register(app, pool) +custom_routes.register(app, pool) + const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) diff --git a/examples/js/express/mysql/custom_routes.js b/examples/js/express/mysql/custom_routes.js new file mode 100644 index 0000000..2f49e76 --- /dev/null +++ b/examples/js/express/mysql/custom_routes.js @@ -0,0 +1,7 @@ +exports.register = (app, pool) => { + + app.get('/custom/route', (req, res, next) => { + res.json({ "custom": true }) + }) + +} diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 809d060..8c50eee 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -1,6 +1,17 @@ +<% +// "custom_routes.js" => "custom_routes" +function removeExtension(filename) { + return filename.replace(/\.js$/, '') +} +-%> const express = require('express') const mysql = require('mysql') const routes = require('./routes') +<% customRouteFilenames.forEach(filename => { + const routerName = removeExtension(filename) +-%> +const <%- routerName %> = require('./<%- routerName %>') +<% }) -%> const app = express() app.use(express.json()) @@ -35,6 +46,11 @@ const pool = mysql.createPool({ }) routes.register(app, pool) +<% customRouteFilenames.forEach(filename => { + const routerName = removeExtension(filename) +%> +<%- routerName %>.register(app, pool) +<% }) -%> const port = process.env.PORT || 3000 app.listen(port, () => { From 87c870a2fab0848e2d4c72e65cc6fcdfe39176e0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 09:13:39 +0700 Subject: [PATCH 287/346] feat(ts): include user defined routes from *_routes.ts files In order to include custom routes: - create a file _routes.ts - export a function "register" that accepts 2 arguments (express router and MySQL connection pool) Part of #27 --- examples/ts/express/mysql/app.ts | 2 ++ examples/ts/express/mysql/custom_routes.ts | 10 ++++++++++ src/templates/app.ts.ejs | 16 ++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 examples/ts/express/mysql/custom_routes.ts diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index 44b749c..b90b2d2 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -2,6 +2,7 @@ import express from 'express' import mysql from 'mysql' const routes = require('./routes') +const custom_routes = require('./custom_routes') const app = express() app.use(express.json()) @@ -34,6 +35,7 @@ const pool = mysql.createPool({ }) routes.register(app, pool) +custom_routes.register(app, pool) const port = process.env.PORT || 3000 app.listen(port, () => { diff --git a/examples/ts/express/mysql/custom_routes.ts b/examples/ts/express/mysql/custom_routes.ts new file mode 100644 index 0000000..b175712 --- /dev/null +++ b/examples/ts/express/mysql/custom_routes.ts @@ -0,0 +1,10 @@ +import { Express, NextFunction, Request, Response } from 'express' +import { Pool } from 'mysql' + +exports.register = (app: Express, pool: Pool) => { + + app.get('/custom/route', (req: Request, res: Response, next: NextFunction) => { + res.json({ "custom": true }) + }) + +} diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index 24289b4..ec5fced 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -1,7 +1,18 @@ +<% +// "custom_routes.ts" => "custom_routes" +function removeExtension(filename) { + return filename.replace(/\.ts$/, '') +} +-%> import express from 'express' import mysql from 'mysql' const routes = require('./routes') +<% customRouteFilenames.forEach(filename => { + const routerName = removeExtension(filename) +-%> +const <%- routerName %> = require('./<%- routerName %>') +<% }) -%> const app = express() app.use(express.json()) @@ -36,6 +47,11 @@ const pool = mysql.createPool({ }) routes.register(app, pool) +<% customRouteFilenames.forEach(filename => { + const routerName = removeExtension(filename) +-%> +<%- routerName %>.register(app, pool) +<% }) -%> const port = process.env.PORT || 3000 app.listen(port, () => { From d23684d3e4263b0fad9d8b1e9a81eaef51d05941 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 09:14:50 +0700 Subject: [PATCH 288/346] refactor(js): group routes together Should be in e4c4814ecfc4ffa20623be4eeccfaea33dfcb764 commit. Relate to #27 --- examples/js/express/mysql/app.js | 1 - src/templates/app.js.ejs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index 8f2be94..f22af03 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -34,7 +34,6 @@ const pool = mysql.createPool({ }) routes.register(app, pool) - custom_routes.register(app, pool) const port = process.env.PORT || 3000 diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 8c50eee..600c71e 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -48,7 +48,7 @@ const pool = mysql.createPool({ routes.register(app, pool) <% customRouteFilenames.forEach(filename => { const routerName = removeExtension(filename) -%> +-%> <%- routerName %>.register(app, pool) <% }) -%> From 5185d217d8eaa2c311a4c23c03e954e708a9f031 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 17 Apr 2024 09:26:46 +0700 Subject: [PATCH 289/346] chore: don't cancel all in-progress and queued jobs in the matrix if any job in the matrix fails Relate to #13 --- .github/workflows/integration-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 32b3712..8848924 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,6 +23,8 @@ jobs: runs-on: ubuntu-20.04 # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs strategy: + # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#handling-failures + fail-fast: false matrix: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations include: From 9eaeb80ccf37e02f664e856420df6db7aa0d38ce Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 18 Apr 2024 07:57:53 +0700 Subject: [PATCH 290/346] feat(golang): include user defined routes from *_routes.go files In order to include custom routes: - create a file _routes.go - define a function "registerRoutes" that accepts 2 arguments (router and database) Part of #27 --- examples/go/chi/mysql/app.go | 1 + examples/go/chi/mysql/custom_routes.go | 21 +++++++++++++++++++++ src/cli.js | 3 ++- src/templates/app.go.ejs | 12 ++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 examples/go/chi/mysql/custom_routes.go diff --git a/examples/go/chi/mysql/app.go b/examples/go/chi/mysql/app.go index cdf9d4f..1702e59 100644 --- a/examples/go/chi/mysql/app.go +++ b/examples/go/chi/mysql/app.go @@ -40,6 +40,7 @@ func main() { r := chi.NewRouter() registerRoutes(r, db) + registerCustomRoutes(r, db) port := os.Getenv("PORT") if port == "" { diff --git a/examples/go/chi/mysql/custom_routes.go b/examples/go/chi/mysql/custom_routes.go new file mode 100644 index 0000000..9b9abc9 --- /dev/null +++ b/examples/go/chi/mysql/custom_routes.go @@ -0,0 +1,21 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi" + "github.com/jmoiron/sqlx" +) + +func registerCustomRoutes(r chi.Router, db *sqlx.DB) { + + r.Get("/custom/route", func(w http.ResponseWriter, r *http.Request) { + result := map[string]bool{ + "custom": true, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&result) + }) + +} diff --git a/src/cli.js b/src/cli.js index a702734..7439712 100755 --- a/src/cli.js +++ b/src/cli.js @@ -104,7 +104,8 @@ const createApp = async (destDir, { lang }) => { `${__dirname}/templates/${fileName}.ejs`, { // @todo #27 Document usage of user defined routes - 'customRouteFilenames': customRouters + 'customRouteFilenames': customRouters, + 'capitalize': capitalize, } ) diff --git a/src/templates/app.go.ejs b/src/templates/app.go.ejs index cdf9d4f..21e447f 100644 --- a/src/templates/app.go.ejs +++ b/src/templates/app.go.ejs @@ -1,3 +1,10 @@ +<% +// "custom_routes.go" => "registerCustomRoutes" +function fileName2registerRouterFunc(filename) { + const routerName = filename.replace(/_routes\.go$/, '') + return `register${capitalize(routerName)}Routes` +} +-%> package main import "fmt" @@ -40,6 +47,11 @@ func main() { r := chi.NewRouter() registerRoutes(r, db) +<% customRouteFilenames.forEach(filename => { + const registerRouterFunc = fileName2registerRouterFunc(filename) +-%> + <%- registerRouterFunc %>(r, db) +<% }) -%> port := os.Getenv("PORT") if port == "" { From 23af83916e2b4ecf4f311b83abf2372f4b814498 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 21 Apr 2024 14:25:21 +0700 Subject: [PATCH 291/346] chore(js): move an exception handler in order to catch exceptions from custom routes Part of #27 Relate to #48 --- examples/js/express/mysql/app.js | 5 +++++ examples/js/express/mysql/custom_routes.js | 4 ++++ examples/js/express/mysql/routes.js | 4 ---- src/templates/app.js.ejs | 5 +++++ src/templates/routes.js.ejs | 4 ---- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/js/express/mysql/app.js b/examples/js/express/mysql/app.js index f22af03..455a152 100644 --- a/examples/js/express/mysql/app.js +++ b/examples/js/express/mysql/app.js @@ -36,6 +36,11 @@ const pool = mysql.createPool({ routes.register(app, pool) custom_routes.register(app, pool) +app.use((error, req, res, next) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) +}) + const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) diff --git a/examples/js/express/mysql/custom_routes.js b/examples/js/express/mysql/custom_routes.js index 2f49e76..84c2454 100644 --- a/examples/js/express/mysql/custom_routes.js +++ b/examples/js/express/mysql/custom_routes.js @@ -4,4 +4,8 @@ exports.register = (app, pool) => { res.json({ "custom": true }) }) + app.get('/custom/exception', (req, res, next) => { + throw new Error('expected err') + }) + } diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index 7a055b4..b5362d6 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -189,10 +189,6 @@ const register = (app, pool) => { ) }) - app.use((error, req, res, next) => { - console.error(error) - res.status(500).json({ "error": "Internal Server Error" }) - }) } exports.register = register diff --git a/src/templates/app.js.ejs b/src/templates/app.js.ejs index 600c71e..9379161 100644 --- a/src/templates/app.js.ejs +++ b/src/templates/app.js.ejs @@ -52,6 +52,11 @@ routes.register(app, pool) <%- routerName %>.register(app, pool) <% }) -%> +app.use((error, req, res, next) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) +}) + const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) diff --git a/src/templates/routes.js.ejs b/src/templates/routes.js.ejs index faa3266..9e00054 100644 --- a/src/templates/routes.js.ejs +++ b/src/templates/routes.js.ejs @@ -86,10 +86,6 @@ endpoints.forEach(function(endpoint) { }) }) %> - app.use((error, req, res, next) => { - console.error(error) - res.status(500).json({ "error": "Internal Server Error" }) - }) } exports.register = register From efdea2af01a1b6961b13399e35427c3e7549c1fd Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 21 Apr 2024 14:34:43 +0700 Subject: [PATCH 292/346] chore(ts): move an exception handler in order to catch exceptions from custom routes Part of #27 Relate to #48 --- examples/ts/express/mysql/app.ts | 6 ++++++ examples/ts/express/mysql/custom_routes.ts | 4 ++++ examples/ts/express/mysql/routes.ts | 4 ---- src/templates/app.ts.ejs | 6 ++++++ src/templates/routes.ts.ejs | 4 ---- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/ts/express/mysql/app.ts b/examples/ts/express/mysql/app.ts index b90b2d2..d97a004 100644 --- a/examples/ts/express/mysql/app.ts +++ b/examples/ts/express/mysql/app.ts @@ -1,4 +1,5 @@ import express from 'express' +import { NextFunction, Request, Response } from 'express' import mysql from 'mysql' const routes = require('./routes') @@ -37,6 +38,11 @@ const pool = mysql.createPool({ routes.register(app, pool) custom_routes.register(app, pool) +app.use((error: any, req: Request, res: Response, next: NextFunction) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) +}) + const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) diff --git a/examples/ts/express/mysql/custom_routes.ts b/examples/ts/express/mysql/custom_routes.ts index b175712..0f4dbff 100644 --- a/examples/ts/express/mysql/custom_routes.ts +++ b/examples/ts/express/mysql/custom_routes.ts @@ -7,4 +7,8 @@ exports.register = (app: Express, pool: Pool) => { res.json({ "custom": true }) }) + app.get('/custom/exception', (req: Request, res: Response, next: NextFunction) => { + throw new Error('expected err') + }) + } diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 9705484..36c6470 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -192,10 +192,6 @@ const register = (app: Express, pool: Pool) => { ) }) - app.use((error: any, req: Request, res: Response, next: NextFunction) => { - console.error(error) - res.status(500).json({ "error": "Internal Server Error" }) - }) } exports.register = register diff --git a/src/templates/app.ts.ejs b/src/templates/app.ts.ejs index ec5fced..3079eeb 100644 --- a/src/templates/app.ts.ejs +++ b/src/templates/app.ts.ejs @@ -5,6 +5,7 @@ function removeExtension(filename) { } -%> import express from 'express' +import { NextFunction, Request, Response } from 'express' import mysql from 'mysql' const routes = require('./routes') @@ -53,6 +54,11 @@ routes.register(app, pool) <%- routerName %>.register(app, pool) <% }) -%> +app.use((error: any, req: Request, res: Response, next: NextFunction) => { + console.error(error) + res.status(500).json({ "error": "Internal Server Error" }) +}) + const port = process.env.PORT || 3000 app.listen(port, () => { console.log(`Listen on ${port}`) diff --git a/src/templates/routes.ts.ejs b/src/templates/routes.ts.ejs index 7319f4f..ea8043e 100644 --- a/src/templates/routes.ts.ejs +++ b/src/templates/routes.ts.ejs @@ -89,10 +89,6 @@ endpoints.forEach(function(endpoint) { }) }) %> - app.use((error: any, req: Request, res: Response, next: NextFunction) => { - console.error(error) - res.status(500).json({ "error": "Internal Server Error" }) - }) } exports.register = register From ce0052a8972b935438892b636ad32234872528a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:57:07 +0000 Subject: [PATCH 293/346] ci: bump actions/checkout from 4.1.2 to 4.1.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 31002e4..e88527c 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 89d66da..78ca035 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index acad99a..70d86e9 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 8432959..19cb1a3 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8848924..3a94c79 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.2 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 9cd131830a0ed36f7092b458ed9d3da2466c9af0 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 22 Apr 2024 09:09:07 +0700 Subject: [PATCH 294/346] feat(python): return JSON response with Internal Server Error for any exceptions Part of #48 --- examples/python/fastapi/postgres/app.py | 10 +++++++++- examples/python/fastapi/postgres/custom_routes.py | 4 ++++ src/templates/app.py.ejs | 10 +++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/python/fastapi/postgres/app.py b/examples/python/fastapi/postgres/app.py index 773f1c9..8c04ac2 100644 --- a/examples/python/fastapi/postgres/app.py +++ b/examples/python/fastapi/postgres/app.py @@ -1,10 +1,18 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse from routes import router from custom_routes import router as custom_router app = FastAPI() +@app.exception_handler(Exception) +async def exception_handler(request: Request, ex: Exception): + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"error": "Internal Server Error"} + ) + app.include_router(router) app.include_router(custom_router) diff --git a/examples/python/fastapi/postgres/custom_routes.py b/examples/python/fastapi/postgres/custom_routes.py index 5bf9232..217571c 100644 --- a/examples/python/fastapi/postgres/custom_routes.py +++ b/examples/python/fastapi/postgres/custom_routes.py @@ -6,3 +6,7 @@ @router.get('/custom/route') def customRoute(): return { "custom": True } + +@router.get('/custom/exception') +def customException(): + raise RuntimeError('expected error') diff --git a/src/templates/app.py.ejs b/src/templates/app.py.ejs index d379863..a1eb08d 100644 --- a/src/templates/app.py.ejs +++ b/src/templates/app.py.ejs @@ -11,7 +11,8 @@ function removeExtension(filename) { } -%> -from fastapi import FastAPI +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse from routes import router <% customRouteFilenames.forEach(filename => { %> from <%= removeExtension(filename) %> import router as <%= fileName2routerName(filename) %> @@ -19,6 +20,13 @@ from <%= removeExtension(filename) %> import router as <%= fileName2routerName(f app = FastAPI() +@app.exception_handler(Exception) +async def exception_handler(request: Request, ex: Exception): + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"error": "Internal Server Error"} + ) + app.include_router(router) <% customRouteFilenames.forEach(filename => { %> app.include_router(<%= fileName2routerName(filename) %>) From b10946d1103d90c6b22e183dde0a55b9af4a0678 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 22 Apr 2024 09:13:24 +0700 Subject: [PATCH 295/346] test: ensure that exceptions are handled and responded accordingly Part of #48 --- tests/misc.hurl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/misc.hurl b/tests/misc.hurl index bb4e330..ba2e3e7 100644 --- a/tests/misc.hurl +++ b/tests/misc.hurl @@ -11,3 +11,10 @@ GET {{ SERVER_URL }}/custom/route HTTP 200 [Asserts] jsonpath "$.custom" == true + + +GET {{ SERVER_URL }}/custom/exception +HTTP 500 +[Asserts] +header "Content-Type" contains "application/json" +jsonpath "$.error" == "Internal Server Error" From db5b4c265b36063a6c7485eabae3265f939de616 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 24 Apr 2024 07:38:40 +0700 Subject: [PATCH 296/346] chore: don't test Go implementation for 500 errors As we don't have a generic error handler, there is no reason to have a test for such behavior. Part of #48 --- .github/workflows/integration-tests.yml | 5 +++++ tests/misc.hurl | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3a94c79..b0489e0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -34,15 +34,19 @@ jobs: - docker-service-name: 'express-js' database-service-name: 'mysql' application-port: 3010 + skip_500_error_testing: false - docker-service-name: 'express-ts' database-service-name: 'mysql' application-port: 3020 + skip_500_error_testing: false - docker-service-name: 'chi' database-service-name: 'mysql' application-port: 3030 + skip_500_error_testing: true - docker-service-name: 'fastapi' database-service-name: 'postgres' application-port: 4040 + skip_500_error_testing: false env: # Prevent interference between builds by setting the project name to a unique value. Otherwise # "docker compose down" has been stopping containers (especially database) from other builds. @@ -92,6 +96,7 @@ jobs: hurl \ --error-format long \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ + --variable skip_500_error_testing=${{ matrix.skip_500_error_testing }} \ --test \ tests/crud.hurl \ tests/misc.hurl diff --git a/tests/misc.hurl b/tests/misc.hurl index ba2e3e7..45b2cc5 100644 --- a/tests/misc.hurl +++ b/tests/misc.hurl @@ -2,7 +2,7 @@ # Tests for various operations # # How to run: -# hurl --variable SERVER_URL=http://127.0.0.1:3000 misc.hurl --test +# hurl --variable SERVER_URL=http://127.0.0.1:3000 --variable skip_500_error_testing=false misc.hurl --test # @@ -14,6 +14,8 @@ jsonpath "$.custom" == true GET {{ SERVER_URL }}/custom/exception +[Options] +skip: {{ skip_500_error_testing }} HTTP 500 [Asserts] header "Content-Type" contains "application/json" From 2fc11145a4ecebd115f9a91d87eae602b6606052 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 24 Apr 2024 08:45:30 +0700 Subject: [PATCH 297/346] fix(python)!: respect $PORT variable instead of to always listen on 8000 Relate to #16 --- README.md | 2 +- docker/docker-compose.yaml | 2 +- examples/python/fastapi/postgres/Dockerfile | 2 +- src/templates/Dockerfile.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 99a3481..f238ed8 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | JavaScript |
$ npm install
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | TypeScript |
$ npm install
$ npm run build
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ npm start
| | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| - | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app
| + | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app --port 3000
| --- :bulb: **NOTE** diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 2a8ca49..80a9ad0 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -96,7 +96,7 @@ services: - DB_HOST=postgres # defaults to localhost - PORT=4040 # defaults to 3000 ports: - - '4040:8000' + - '4040:4040' depends_on: postgres: condition: service_healthy diff --git a/examples/python/fastapi/postgres/Dockerfile b/examples/python/fastapi/postgres/Dockerfile index 312f62b..4a65ff4 100644 --- a/examples/python/fastapi/postgres/Dockerfile +++ b/examples/python/fastapi/postgres/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /opt/app COPY requirements.txt ./ RUN pip install --no-cache-dir --upgrade -r requirements.txt COPY *.py ./ -CMD [ "uvicorn", "app:app", "--host", "0.0.0.0" ] +CMD [ "sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-3000}" ] diff --git a/src/templates/Dockerfile.py b/src/templates/Dockerfile.py index 312f62b..4a65ff4 100644 --- a/src/templates/Dockerfile.py +++ b/src/templates/Dockerfile.py @@ -3,4 +3,4 @@ COPY requirements.txt ./ RUN pip install --no-cache-dir --upgrade -r requirements.txt COPY *.py ./ -CMD [ "uvicorn", "app:app", "--host", "0.0.0.0" ] +CMD [ "sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-3000}" ] From dadcbd2a5c850799c9ac2b673ec84a3022539248 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 24 Apr 2024 10:35:38 +0700 Subject: [PATCH 298/346] chore: release of 0.0.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97181fb..9ea4c5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "query2app", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index ede504f..5994ce7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "query2app", - "version": "0.0.2", + "version": "0.0.3", "description": "Generates the endpoints from SQL -> URL mapping", "keywords": [ "sql", From 1feb0fc559f538a31931e0173e00c1b6aa260286 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 25 Apr 2024 21:21:52 +0700 Subject: [PATCH 299/346] chore!: rename npm goals - npm run gen-js-example => npm run example:js - npm run gen-ts-example => npm run example:ts - npm run gen-go-example => npm run example:go - npm run gen-py-example => npm run example:py --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- package.json | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index e88527c..91f6b72 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -39,7 +39,7 @@ jobs: run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Golang + Chi application - run: npm run gen-go-example + run: npm run example:go - name: Check whether all modified files have been committed run: >- diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 78ca035..cecf723 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -39,7 +39,7 @@ jobs: run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate JavaScript + Express application - run: npm run gen-js-example + run: npm run example:js - name: Check whether all modified files have been committed run: >- diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 70d86e9..6b26e0a 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -39,7 +39,7 @@ jobs: run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate Python + FastAPI application - run: npm run gen-py-example + run: npm run example:py - name: Check whether all modified files have been committed run: >- diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 19cb1a3..83242f7 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -39,7 +39,7 @@ jobs: run: npm ci --no-audit --no-fund # https://docs.npmjs.com/cli/v8/commands/npm-ci - name: Generate TypeScript + Express application - run: npm run gen-ts-example + run: npm run example:ts - name: Check whether all modified files have been committed run: >- diff --git a/package.json b/package.json index 5994ce7..161e96a 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,10 @@ "src/**" ], "scripts": { - "gen-js-example": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", - "gen-ts-example": "cd examples/ts/express/mysql; ../../../../src/cli.js --lang ts", - "gen-go-example": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", - "gen-py-example": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" + "example:js": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", + "example:ts": "cd examples/ts/express/mysql; ../../../../src/cli.js --lang ts", + "example:go": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", + "example:py": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" }, "dependencies": { "ejs": "~3.1.9", From e6b64587cec15b6c91b71e0891c350bfaf998ab2 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 25 Apr 2024 21:24:15 +0700 Subject: [PATCH 300/346] chore: add "example:all" npm goal to generate all examples --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 161e96a..a7b6a91 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "src/**" ], "scripts": { + "example:all": "npm run example:js && npm run example:ts && npm run example:go && npm run example:py", "example:js": "cd examples/js/express/mysql; ../../../../src/cli.js --lang js", "example:ts": "cd examples/ts/express/mysql; ../../../../src/cli.js --lang ts", "example:go": "cd examples/go/chi/mysql; ../../../../src/cli.js --lang go", From 5c4a3256bc3a48016aed4f783ae692aeb301d893 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 25 Apr 2024 21:28:06 +0700 Subject: [PATCH 301/346] refactor(python): transform db.py to a template Part of #53 --- src/cli.js | 6 +++++- src/templates/{db.py => db.py.ejs} | 0 2 files changed, 5 insertions(+), 1 deletion(-) rename src/templates/{db.py => db.py.ejs} (100%) diff --git a/src/cli.js b/src/cli.js index 7439712..d445c92 100755 --- a/src/cli.js +++ b/src/cli.js @@ -120,7 +120,11 @@ const createDb = async (destDir, { lang }) => { console.log('Generate', fileName) const resultFile = path.join(destDir, fileName) - return fsPromises.copyFile(`${__dirname}/templates/${fileName}`, resultFile) + const resultedCode = await ejs.renderFile( + `${__dirname}/templates/${fileName}.ejs` + ) + + return fsPromises.writeFile(resultFile, resultedCode) } // "-- comment\nSELECT * FROM foo" => "SELECT * FROM foo" diff --git a/src/templates/db.py b/src/templates/db.py.ejs similarity index 100% rename from src/templates/db.py rename to src/templates/db.py.ejs From fbcc711ca643e1954f4c6ceb048467b5c2879283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 23:47:29 +0000 Subject: [PATCH 302/346] ci: bump actions/checkout from 4.1.3 to 4.1.4 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 91f6b72..ff5e461 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index cecf723..bcac058 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 6b26e0a..2889bda 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 83242f7..0937089 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b0489e0..3d8ff05 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.3 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 2b2f64310eff2c51e4039307f615bc4fe8570357 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 28 Apr 2024 07:44:08 +0700 Subject: [PATCH 303/346] refactor: extract hurl version to a variable --- .github/workflows/integration-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3d8ff05..6efa4ad 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -84,8 +84,9 @@ jobs: - name: Install Hurl run: | - DEB=hurl_4.2.0_amd64.deb - curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.2.0/$DEB + VER=4.2.0 + DEB=hurl_${VER}_amd64.deb + curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB sudo dpkg --install $DEB - name: Show Hurl version From 41f254bd07bc1ecc4d6c433a828332e88ba882f3 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 28 Apr 2024 07:44:37 +0700 Subject: [PATCH 304/346] ci: update hurl to 4.3.0 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6efa4ad..6078ca9 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -84,7 +84,7 @@ jobs: - name: Install Hurl run: | - VER=4.2.0 + VER=4.3.0 DEB=hurl_${VER}_amd64.deb curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB sudo dpkg --install $DEB From a74adb5479de3dc7b937fa121da17dff230dd633 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 30 Apr 2024 07:33:04 +0700 Subject: [PATCH 305/346] chore(python): add --port option to the instruction Should be in 2fc11145a4ecebd115f9a91d87eae602b6606052 commit. Relate to #16 --- src/generator/PyGenerator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generator/PyGenerator.js b/src/generator/PyGenerator.js index 1d871e0..14f5b6f 100644 --- a/src/generator/PyGenerator.js +++ b/src/generator/PyGenerator.js @@ -5,7 +5,7 @@ module.exports = class PyGenerator { pip install -r requirements.txt to install its dependencies and export DB_NAME=db DB_USER=user DB_PASSWORD=secret - uvicorn app:app + uvicorn app:app --port 3000 afteward to run` } From 940643f1eb898dad3176d7d8d3c11a5d61f64ef1 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 30 Apr 2024 07:36:31 +0700 Subject: [PATCH 306/346] refactor: use CURRENT_TIMESTAMP instead of NOW() in order to make it work on SQLite Part of #53 --- examples/go/chi/mysql/routes.go | 6 +++--- examples/js/express/mysql/endpoints.yaml | 6 +++--- examples/js/express/mysql/routes.js | 6 +++--- examples/python/fastapi/postgres/routes.py | 6 +++--- examples/ts/express/mysql/routes.ts | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/go/chi/mysql/routes.go b/examples/go/chi/mysql/routes.go index 4bf7012..d468a64 100644 --- a/examples/go/chi/mysql/routes.go +++ b/examples/go/chi/mysql/routes.go @@ -137,9 +137,9 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { , :name_ru , :slug , :hidden - , NOW() + , CURRENT_TIMESTAMP , :user_id - , NOW() + , CURRENT_TIMESTAMP , :user_id )`, args, @@ -235,7 +235,7 @@ func registerRoutes(r chi.Router, db *sqlx.DB) { , name_ru = :name_ru , slug = :slug , hidden = :hidden - , updated_at = NOW() + , updated_at = CURRENT_TIMESTAMP , updated_by = :user_id WHERE id = :categoryId`, args, diff --git a/examples/js/express/mysql/endpoints.yaml b/examples/js/express/mysql/endpoints.yaml index 7afd1f5..eb59f3c 100644 --- a/examples/js/express/mysql/endpoints.yaml +++ b/examples/js/express/mysql/endpoints.yaml @@ -64,9 +64,9 @@ , :b.name_ru , :b.slug , :b.hidden - , NOW() + , CURRENT_TIMESTAMP , :b.user_id - , NOW() + , CURRENT_TIMESTAMP , :b.user_id ) dto: @@ -122,7 +122,7 @@ , name_ru = :b.name_ru , slug = :b.slug , hidden = :b.hidden - , updated_at = NOW() + , updated_at = CURRENT_TIMESTAMP , updated_by = :b.user_id WHERE id = :p.categoryId dto: diff --git a/examples/js/express/mysql/routes.js b/examples/js/express/mysql/routes.js index b5362d6..5e345f4 100644 --- a/examples/js/express/mysql/routes.js +++ b/examples/js/express/mysql/routes.js @@ -78,9 +78,9 @@ const register = (app, pool) => { , :name_ru , :slug , :hidden - , NOW() + , CURRENT_TIMESTAMP , :user_id - , NOW() + , CURRENT_TIMESTAMP , :user_id )`, { @@ -152,7 +152,7 @@ const register = (app, pool) => { , name_ru = :name_ru , slug = :slug , hidden = :hidden - , updated_at = NOW() + , updated_at = CURRENT_TIMESTAMP , updated_by = :user_id WHERE id = :categoryId`, { diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index 0cbdbc3..e86f7cd 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -117,9 +117,9 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): , %(name_ru)s , %(slug)s , %(hidden)s - , NOW() + , CURRENT_TIMESTAMP , %(user_id)s - , NOW() + , CURRENT_TIMESTAMP , %(user_id)s ) """, { @@ -192,7 +192,7 @@ def put_v1_categories_category_id(body: CreateCategoryDto, categoryId, conn=Depe , name_ru = %(name_ru)s , slug = %(slug)s , hidden = %(hidden)s - , updated_at = NOW() + , updated_at = CURRENT_TIMESTAMP , updated_by = %(user_id)s WHERE id = %(categoryId)s """, { diff --git a/examples/ts/express/mysql/routes.ts b/examples/ts/express/mysql/routes.ts index 36c6470..57a28a1 100644 --- a/examples/ts/express/mysql/routes.ts +++ b/examples/ts/express/mysql/routes.ts @@ -81,9 +81,9 @@ const register = (app: Express, pool: Pool) => { , :name_ru , :slug , :hidden - , NOW() + , CURRENT_TIMESTAMP , :user_id - , NOW() + , CURRENT_TIMESTAMP , :user_id )`, { @@ -155,7 +155,7 @@ const register = (app: Express, pool: Pool) => { , name_ru = :name_ru , slug = :slug , hidden = :hidden - , updated_at = NOW() + , updated_at = CURRENT_TIMESTAMP , updated_by = :user_id WHERE id = :categoryId`, { From ccb5c76bc9126e849318904cbfde9e5d7bd45252 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 30 Apr 2024 09:25:01 +0700 Subject: [PATCH 307/346] feat(python): specify variable type for query parameters Part of #53 --- examples/python/fastapi/postgres/routes.py | 2 +- src/templates/routes.py.ejs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/python/fastapi/postgres/routes.py b/examples/python/fastapi/postgres/routes.py index e86f7cd..7499161 100644 --- a/examples/python/fastapi/postgres/routes.py +++ b/examples/python/fastapi/postgres/routes.py @@ -134,7 +134,7 @@ def post_v1_categories(body: CreateCategoryDto, conn=Depends(db_connection)): @router.get('/v1/categories/search') -def get_list_v1_categories_search(hidden, conn=Depends(db_connection)): +def get_list_v1_categories_search(hidden: bool, conn=Depends(db_connection)): try: with conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: diff --git a/src/templates/routes.py.ejs b/src/templates/routes.py.ejs index 2f23cdd..fe5c12d 100644 --- a/src/templates/routes.py.ejs +++ b/src/templates/routes.py.ejs @@ -85,6 +85,18 @@ function findOutType(fieldsInfo, fieldName) { return 'str' } +// "q.title" => "q.title: str" +// "q.active" => "q.active: bool" +// "q.age" => "q.age: int" +// "p.id" => "p.id" +// "b.name" => "b.name" +function appendVariableTypeToQueryParam(paramsInfo, varName) { + if (varName.startsWith('q.')) { + return `${varName}: ${findOutType(paramsInfo, stipOurPrefixes(varName))}` + } + return varName +} + // LATER: reduce duplication with routes.go.ejs function addTypes(props, fieldsInfo) { return props.map(prop => { @@ -187,7 +199,8 @@ endpoints.forEach(function(endpoint) { const pythonMethodName = generate_method_name(method.name, path) // LATER: add support for aggregated_queries (#17) - const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(stipOurPrefixes) : [] + const queryParamsInfo = method.params && method.params.query ? method.params.query : {} + const argsFromQuery = method.query ? extractParamsFromQuery(method.query).map(param => appendVariableTypeToQueryParam(queryParamsInfo, param)).map(stipOurPrefixes) : [] // define before "if", to make them available later let methodArgs From ea65af96dcaef5843c7e732a0219cc488029a146 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 4 May 2024 09:45:57 +0700 Subject: [PATCH 308/346] docs: highlight notes and warnings See https://github.com/orgs/community/discussions/16925 [skip ci] --- README.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f238ed8..f051c07 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Query To App Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) ->:warning: This is a proof of concept at this moment. Until it reaches a stable version, it might (and will) break a compatibility. +> [!WARNING] +> This is a proof of concept at this moment. Until it reaches a stable version, it might (and will) break a compatibility. # How to use @@ -67,20 +68,14 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app --port 3000
| - --- - :bulb: **NOTE** + > [!TIP] + > While the example used `export` for setting up the environment variables, we don't recommend export variables that way! This was provided as an example to illustrate that an application follows [The Twelve Factors](https://12factor.net/config) and can be configured by passing environment variables. In real life, you will use docker, docker-compose, Kubernetes or other ways to run an app with required environment variables. - While the example used `export` for setting up the environment variables, we don't recommend export variables that way! This was provided as an example to illustrate that an application follows [The Twelve Factors](https://12factor.net/config) and can be configured by passing environment variables. In real life, you will use docker, docker-compose, Kubernetes or other ways to run an app with required environment variables. - - --- - :bulb: **NOTE** - - An app also supports other environment variables: - - * `PORT`: a port to listen (defaults to `3000`) - * `DB_HOST` a database host (defaults to `localhost`) - - --- + > [!NOTE] + > An app also supports other environment variables: + > + > * `PORT`: a port to listen (defaults to `3000`) + > * `DB_HOST` a database host (defaults to `localhost`) 1. Test that it works From 62339718da3934ad5b632ec33ec406555020cd67 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sat, 4 May 2024 09:53:40 +0700 Subject: [PATCH 309/346] chore: partially revert previous changes as notes doesn't work inside
Correction for ea65af96dcaef5843c7e732a0219cc488029a146 commit. [skip ci] --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f051c07..9e0f2d0 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,20 @@ Generates the endpoints (or a whole app) from a mapping (SQL query -> URL) | Golang |
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ go run *.go
or
$ go build -o app
$ ./app
| | Python |
$ pip install -r requirements.txt
$ export DB_NAME=my-db DB_USER=my-user DB_PASSWORD=my-password
$ uvicorn app:app --port 3000
| - > [!TIP] - > While the example used `export` for setting up the environment variables, we don't recommend export variables that way! This was provided as an example to illustrate that an application follows [The Twelve Factors](https://12factor.net/config) and can be configured by passing environment variables. In real life, you will use docker, docker-compose, Kubernetes or other ways to run an app with required environment variables. + --- + :bulb: **NOTE** - > [!NOTE] - > An app also supports other environment variables: - > - > * `PORT`: a port to listen (defaults to `3000`) - > * `DB_HOST` a database host (defaults to `localhost`) + While the example used `export` for setting up the environment variables, we don't recommend export variables that way! This was provided as an example to illustrate that an application follows [The Twelve Factors](https://12factor.net/config) and can be configured by passing environment variables. In real life, you will use docker, docker-compose, Kubernetes or other ways to run an app with required environment variables. + + --- + :bulb: **NOTE** + + An app also supports other environment variables: + + * `PORT`: a port to listen (defaults to `3000`) + * `DB_HOST` a database host (defaults to `localhost`) + + ---
1. Test that it works From 2c043c607bb874e4d15cfaa9ad8fdd0e9f77630b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 23:26:35 +0000 Subject: [PATCH 310/346] ci: bump actions/checkout from 4.1.4 to 4.1.6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.4...v4.1.6) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index ff5e461..7fe9634 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index bcac058..707f8a0 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 2889bda..75aadc2 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 0937089..18e5a67 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6078ca9..a9612a8 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.4 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 5f622b0ed1ad0cbb767bf11dddb615783f14c561 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 23 May 2024 21:21:15 +0700 Subject: [PATCH 311/346] ci: configure dependabot to monitor updates for NPM Part of #38 [skip ci] --- .github/dependabot.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a9aab58..9e41b33 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,3 +24,23 @@ updates: labels: [ "kind/dependency-update", "area/ci" ] # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit open-pull-requests-limit: 1 + + - package-ecosystem: "npm" + directory: "/" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#scheduleinterval + schedule: + interval: "daily" + time: "06:00" + timezone: "Asia/Novosibirsk" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#commit-message + commit-message: + prefix: "build" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy + versioning-strategy: "increase" + assignees: [ "php-coder" ] + reviewers: [ "php-coder" ] + labels: [ "kind/dependency-update" ] + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#rebase-strategy + rebase-strategy: "disabled" + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit + open-pull-requests-limit: 1 From 7416c2a13f522edf20c7b657caf9d092b01300cc Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 23 May 2024 21:22:11 +0700 Subject: [PATCH 312/346] chore: allow dependabot to update all actions rather than manually specified Relate to #38 [skip ci] --- .github/dependabot.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9e41b33..e6f2ec2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,11 +6,6 @@ updates: - package-ecosystem: "github-actions" directory: "/" - # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#allow - allow: - - dependency-name: "actions/checkout" - - dependency-name: "actions/setup-node" - - dependency-name: "actions/upload-artifact" # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#scheduleinterval schedule: interval: "daily" From a352f702c09dcf153210d5cb902e7376af443885 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 23 May 2024 21:23:47 +0700 Subject: [PATCH 313/346] chore: disable auto rebase for update of GitHub Actions Relate to #38 [skip ci] --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e6f2ec2..95a8d66 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,6 +17,8 @@ updates: assignees: [ "php-coder" ] reviewers: [ "php-coder" ] labels: [ "kind/dependency-update", "area/ci" ] + # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#rebase-strategy + rebase-strategy: "disabled" # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit open-pull-requests-limit: 1 From 9b2cbd23dadd7b6f29608331959bf18b889a55b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 14:25:47 +0000 Subject: [PATCH 314/346] chore(deps): bump ejs from 3.1.9 to 3.1.10 Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ea4c5e..e231f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "packages": { "": { "name": "query2app", - "version": "0.0.2", + "version": "0.0.3", "license": "GPL-2.0", "dependencies": { - "ejs": "~3.1.9", + "ejs": "~3.1.10", "js-yaml": "~3.14.0", "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" @@ -104,9 +104,9 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dependencies": { "jake": "^10.8.5" }, @@ -309,9 +309,9 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "requires": { "jake": "^10.8.5" } diff --git a/package.json b/package.json index a7b6a91..6382885 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "example:py": "cd examples/python/fastapi/postgres; ../../../../src/cli.js --lang python" }, "dependencies": { - "ejs": "~3.1.9", + "ejs": "~3.1.10", "js-yaml": "~3.14.0", "minimist": "~1.2.8", "node-sql-parser": "~3.0.4" From 8d04249398af3abbfcfb0f4ab7bd1a6b8328396e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:27:53 +0000 Subject: [PATCH 315/346] ci: bump actions/checkout from 4.1.6 to 4.1.7 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.6 to 4.1.7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.6...v4.1.7) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 7fe9634..1103b0c 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 707f8a0..30097c2 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 75aadc2..e7e881e 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 18e5a67..e3e410f 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a9612a8..cfcf580 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.6 # https://github.com/actions/checkout + uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From bf25a7337fb6a3c7e19c2b4a1eeeaffbdb52201f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:20:46 +0000 Subject: [PATCH 316/346] ci: bump actions/setup-node from 4.0.2 to 4.0.3 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.0.2...v4.0.3) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 1103b0c..0637c99 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 30097c2..370f4a2 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index e7e881e..4f06fc4 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index e3e410f..f9f6e23 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.2 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From f54ce64ba1b81d104f9a1b6544a4b0dd45c6ee0d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Fri, 30 Aug 2024 21:03:45 +0700 Subject: [PATCH 317/346] ci: update hurl to 5.0.1 --- .github/workflows/integration-tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index cfcf580..3b0503d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -84,7 +84,7 @@ jobs: - name: Install Hurl run: | - VER=4.3.0 + VER=5.0.1 DEB=hurl_${VER}_amd64.deb curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB sudo dpkg --install $DEB @@ -98,9 +98,7 @@ jobs: --error-format long \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --variable skip_500_error_testing=${{ matrix.skip_500_error_testing }} \ - --test \ - tests/crud.hurl \ - tests/misc.hurl + --test tests/ - name: Show application logs if: failure() From 61a6b9f83f8a79623ec2c7c57e9105cfc0db0f81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:45:35 +0000 Subject: [PATCH 318/346] ci: bump actions/setup-node from 4.0.3 to 4.0.4 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.3 to 4.0.4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.0.3...v4.0.4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 0637c99..3eafc95 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 370f4a2..9905f96 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 4f06fc4..89e9fda 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index f9f6e23..0e47a56 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.3 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 358afa796a43380b03f4a859b384212b38fd9ee8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:03:18 +0000 Subject: [PATCH 319/346] ci: bump actions/checkout from 4.1.7 to 4.2.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 3eafc95..2997680 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 9905f96..31adc9a 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 89e9fda..403af98 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 0e47a56..9fdd556 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3b0503d..41e9f54 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.1.7 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 6936cfb4fbc512062337431d51438091448e6acd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:13:13 +0000 Subject: [PATCH 320/346] ci: bump actions/checkout from 4.2.0 to 4.2.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.2.0...v4.2.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 2997680..b8a4e8f 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 31adc9a..28a2fa9 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 403af98..649d8ff 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 9fdd556..5363eb8 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 41e9f54..5d1957c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.0 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 2f345661031cc0bcec3da26f742982e3a9ef1a34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:28:11 +0000 Subject: [PATCH 321/346] ci: bump actions/checkout from 4.2.1 to 4.2.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.2.1...v4.2.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index b8a4e8f..d53bb21 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 28a2fa9..09ba90c 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 649d8ff..0b1fd6c 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 5363eb8..6f9aeb7 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5d1957c..7f01a21 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,7 +56,7 @@ jobs: steps: - name: Clone source code - uses: actions/checkout@v4.2.1 # https://github.com/actions/checkout + uses: actions/checkout@v4.2.2 # https://github.com/actions/checkout with: # Whether to configure the token or SSH key with the local git config. Default: true persist-credentials: false From 1eeedc0165f15d3563718e1c82af6d911bf1a543 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Oct 2024 23:05:35 +0000 Subject: [PATCH 322/346] ci: bump actions/setup-node from 4.0.4 to 4.1.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.4 to 4.1.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.0.4...v4.1.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index d53bb21..25dd22a 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 09ba90c..0cc1675 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 0b1fd6c..2bb0fcb 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 6f9aeb7..cddd6c4 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.0.4 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 49a6a27b30fe9ea9e7241a02c7798500d040c31d Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 10 Dec 2024 07:54:09 +0700 Subject: [PATCH 323/346] ci: update Hurl to 6.0.0 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7f01a21..7692ad2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -84,7 +84,7 @@ jobs: - name: Install Hurl run: | - VER=5.0.1 + VER=6.0.0 DEB=hurl_${VER}_amd64.deb curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB sudo dpkg --install $DEB From d948b8a7e17be0d487c5e136a83cc6bad407179c Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Tue, 10 Dec 2024 08:00:00 +0700 Subject: [PATCH 324/346] chore: remove obsoleted "version" parameter time="2024-12-10T00:56:09Z" level=warning msg="docker/docker-compose.yaml: `version` is obsolete" --- docker/docker-compose.local.yaml | 1 - docker/docker-compose.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/docker/docker-compose.local.yaml b/docker/docker-compose.local.yaml index 3bedabb..9d72791 100644 --- a/docker/docker-compose.local.yaml +++ b/docker/docker-compose.local.yaml @@ -7,7 +7,6 @@ # # docker compose -f docker-compose.yaml -f docker-compose.local.yaml up -d # -version: '3' services: mysql: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 80a9ad0..44d2420 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -4,7 +4,6 @@ # docker compose exec mysql mysql -u test -ptest test -e 'SELECT * FROM categories' # docker compose exec postgres psql -U test -c 'SELECT * FROM categories' # -version: '3' services: mysql: From f54d7c78e56366bedcbcdd8a1063c0c752f6fd09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:55:24 +0000 Subject: [PATCH 325/346] ci: bump actions/setup-node from 4.1.0 to 4.2.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 25dd22a..2fd9eb2 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 0cc1675..53ce343 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 2bb0fcb..e7ab7de 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index cddd6c4..9054aee 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.1.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 8b7e266bba8f20c035de1c722e3aaa6bca82daab Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 12 Mar 2025 13:00:57 +0700 Subject: [PATCH 326/346] build: use mise for managing required tools [skip ci] --- mise.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..4641a9c --- /dev/null +++ b/mise.toml @@ -0,0 +1,5 @@ +[tools] +go = "1.14.15" +hurl = "6.0.0" +node = "18.12.0" +python = "3.7.17" From 813418a5b18d8dacfd2b57b39babf839c8a1a76a Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 13 Mar 2025 12:58:55 +0700 Subject: [PATCH 327/346] chore: require hurl from tests folder [skip ci] --- mise.toml | 1 - tests/mise.toml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tests/mise.toml diff --git a/mise.toml b/mise.toml index 4641a9c..560d20e 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,4 @@ [tools] go = "1.14.15" -hurl = "6.0.0" node = "18.12.0" python = "3.7.17" diff --git a/tests/mise.toml b/tests/mise.toml new file mode 100644 index 0000000..1c63298 --- /dev/null +++ b/tests/mise.toml @@ -0,0 +1,2 @@ +[tools] +hurl = "6.0.0" From 8202bc68a2e078f6ae7fc78a3ea0f588c76ef558 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:30:11 +0000 Subject: [PATCH 328/346] ci: bump actions/setup-node from 4.2.0 to 4.3.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 2fd9eb2..7088de9 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 53ce343..0f2da22 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index e7ab7de..83b8854 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 9054aee..5fb5fb5 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.2.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 05e6649a6b12cd0d24a5b2858f070a08655a33c7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 14 Apr 2025 11:11:48 +0700 Subject: [PATCH 329/346] ci: update Hurl to 6.1.1 --- .github/workflows/integration-tests.yml | 2 +- tests/mise.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7692ad2..ab7590d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -84,7 +84,7 @@ jobs: - name: Install Hurl run: | - VER=6.0.0 + VER=6.1.1 DEB=hurl_${VER}_amd64.deb curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB sudo dpkg --install $DEB diff --git a/tests/mise.toml b/tests/mise.toml index 1c63298..89c7edf 100644 --- a/tests/mise.toml +++ b/tests/mise.toml @@ -1,2 +1,2 @@ [tools] -hurl = "6.0.0" +hurl = "6.1.1" From 1db016b361f939bee94e4c66008f8032affd0e34 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 14 Apr 2025 11:48:02 +0700 Subject: [PATCH 330/346] ci: use mise to install Hurl on CI --- .github/workflows/integration-tests.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ab7590d..d06b6ac 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -82,12 +82,19 @@ jobs: working-directory: docker run: docker compose ps - - name: Install Hurl - run: | - VER=6.1.1 - DEB=hurl_${VER}_amd64.deb - curl --location --no-progress-meter --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VER/$DEB - sudo dpkg --install $DEB + - name: Install mise to install Hurl + uses: jdx/mise-action@v2.1.11 # https://github.com/jdx/mise-action + with: + version: 2025.4.2 # [default: latest] mise version to install + install: true # [default: true] run `mise install` + cache: true # [default: true] cache mise using GitHub's cache + log_level: info # [default: info] log level + working_directory: tests # [default: .] directory to run mise in + env: + # Workaround: don't install some dependencies that we don't use (go, node, python) + # See: https://github.com/jdx/mise-action/issues/183 + # https://mise.jdx.dev/configuration/settings.html#disable_tools + MISE_DISABLE_TOOLS: go,node,python - name: Show Hurl version run: hurl --version From 283c0801827fa6545c05a6ac754b88414b23ad3f Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 14 Apr 2025 11:54:16 +0700 Subject: [PATCH 331/346] chore: invoke hurl within tests/ directory in order to use version installed by mise Correction for 1db016b361f939bee94e4c66008f8032affd0e34 commit. --- .github/workflows/integration-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d06b6ac..b6b1696 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -97,15 +97,17 @@ jobs: MISE_DISABLE_TOOLS: go,node,python - name: Show Hurl version + working-directory: tests run: hurl --version - name: Run integration tests + working-directory: tests run: >- hurl \ --error-format long \ --variable SERVER_URL=http://127.0.0.1:${{ matrix.application-port }} \ --variable skip_500_error_testing=${{ matrix.skip_500_error_testing }} \ - --test tests/ + --test - name: Show application logs if: failure() From f3aa2ad81701f32baec1050b57977d2c78bd3730 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 14 Apr 2025 12:05:37 +0700 Subject: [PATCH 332/346] chore: run integration tests under Ubuntu 22.04 in order to have Glibc >= 2.32.0 The error was: /home/runner/.local/share/mise/installs/hurl/6.1.1/hurl-6.1.1-x86_64-unknown-linux-gnu/bin/hurl: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by /home/runner/.local/share/mise/installs/hurl/6.1.1/hurl-6.1.1-x86_64-unknown-linux-gnu/bin/hurl) Correction for 1db016b361f939bee94e4c66008f8032affd0e34 commit. --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b6b1696..e39d696 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -20,7 +20,7 @@ jobs: run-integration-tests: name: Integration Tests # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs strategy: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#handling-failures From 6b2dbfcea6a01a2fd1ba636e59b4a5fa038a7436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:34:17 +0000 Subject: [PATCH 333/346] ci: bump actions/setup-node from 4.3.0 to 4.4.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index 7088de9..deb9f99 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.4.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index 0f2da22..e34dc0f 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.4.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index 83b8854..e4ca844 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.4.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index 5fb5fb5..b199d6a 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup NodeJS - uses: actions/setup-node@v4.3.0 # https://github.com/actions/setup-node + uses: actions/setup-node@v4.4.0 # https://github.com/actions/setup-node with: node-version: 18 cache: 'npm' From 9cceef12bfa16f01ee0ee484fadfc83de1c072ad Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 16 Apr 2025 12:11:31 +0700 Subject: [PATCH 334/346] ci: update mise to 2025.4.4 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.4.3 - https://github.com/jdx/mise/releases/tag/v2025.4.4 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e39d696..6981b49 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.1.11 # https://github.com/jdx/mise-action with: - version: 2025.4.2 # [default: latest] mise version to install + version: 2025.4.4 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From 715f00cc509035c06c41ab38698c4bf47b768ad2 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 17 Apr 2025 10:59:08 +0700 Subject: [PATCH 335/346] ci: run workflows on Ubuntu 22.04 as 20.04 has been retired --- .github/workflows/generate-go-app.yml | 2 +- .github/workflows/generate-js-app.yml | 2 +- .github/workflows/generate-python-app.yml | 2 +- .github/workflows/generate-ts-app.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-go-app.yml b/.github/workflows/generate-go-app.yml index deb9f99..b74fc9e 100644 --- a/.github/workflows/generate-go-app.yml +++ b/.github/workflows/generate-go-app.yml @@ -20,7 +20,7 @@ jobs: generate-app: name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone source code diff --git a/.github/workflows/generate-js-app.yml b/.github/workflows/generate-js-app.yml index e34dc0f..46f3369 100644 --- a/.github/workflows/generate-js-app.yml +++ b/.github/workflows/generate-js-app.yml @@ -20,7 +20,7 @@ jobs: generate-app: name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone source code diff --git a/.github/workflows/generate-python-app.yml b/.github/workflows/generate-python-app.yml index e4ca844..cbe2bfd 100644 --- a/.github/workflows/generate-python-app.yml +++ b/.github/workflows/generate-python-app.yml @@ -20,7 +20,7 @@ jobs: generate-app: name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone source code diff --git a/.github/workflows/generate-ts-app.yml b/.github/workflows/generate-ts-app.yml index b199d6a..a312862 100644 --- a/.github/workflows/generate-ts-app.yml +++ b/.github/workflows/generate-ts-app.yml @@ -20,7 +20,7 @@ jobs: generate-app: name: Generate app # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone source code From eb2fd24e0670b960aff58de2ee88888d99ea258d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 23:58:37 +0000 Subject: [PATCH 336/346] ci: bump jdx/mise-action from 2.1.11 to 2.2.1 Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2.1.11 to 2.2.1. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2.1.11...v2.2.1) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: 2.2.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6981b49..e2a2308 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: run: docker compose ps - name: Install mise to install Hurl - uses: jdx/mise-action@v2.1.11 # https://github.com/jdx/mise-action + uses: jdx/mise-action@v2.2.1 # https://github.com/jdx/mise-action with: version: 2025.4.4 # [default: latest] mise version to install install: true # [default: true] run `mise install` From ec01b4166f90409b94d9f1aa0d83d016d1b7c3fa Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Thu, 24 Apr 2025 12:07:55 +0700 Subject: [PATCH 337/346] ci: update mise to 2025.4.8 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.4.5 - https://github.com/jdx/mise/releases/tag/v2025.4.6 - https://github.com/jdx/mise/releases/tag/v2025.4.7 - https://github.com/jdx/mise/releases/tag/v2025.4.8 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e2a2308..2fb7088 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.2.1 # https://github.com/jdx/mise-action with: - version: 2025.4.4 # [default: latest] mise version to install + version: 2025.4.8 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From 4bb567b1c5bca010ecd06c1f1105fb8cd7f0d102 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 11 May 2025 12:35:50 +0700 Subject: [PATCH 338/346] ci: update mise to 2025.5.3 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.4.9 - https://github.com/jdx/mise/releases/tag/v2025.4.10 - https://github.com/jdx/mise/releases/tag/v2025.4.11 - https://github.com/jdx/mise/releases/tag/v2025.4.12 - https://github.com/jdx/mise/releases/tag/v2025.5.0 - https://github.com/jdx/mise/releases/tag/v2025.5.1 - https://github.com/jdx/mise/releases/tag/v2025.5.2 - https://github.com/jdx/mise/releases/tag/v2025.5.3 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 2fb7088..332e3b7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.2.1 # https://github.com/jdx/mise-action with: - version: 2025.4.8 # [default: latest] mise version to install + version: 2025.5.3 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From a6384ab4d00a92d5278fa0512dad6c33b44be06a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 23:20:22 +0000 Subject: [PATCH 339/346] ci: bump jdx/mise-action from 2.2.1 to 2.2.2 Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: 2.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 332e3b7..10b222b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: run: docker compose ps - name: Install mise to install Hurl - uses: jdx/mise-action@v2.2.1 # https://github.com/jdx/mise-action + uses: jdx/mise-action@v2.2.2 # https://github.com/jdx/mise-action with: version: 2025.5.3 # [default: latest] mise version to install install: true # [default: true] run `mise install` From af24fed361d77a3b904d3d64c67c159b79b25f54 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 14 May 2025 09:57:37 +0700 Subject: [PATCH 340/346] chore: remove deprecated reviewers parameter from dependabot configuration As per dependabot's comment: % The reviewers field in the dependabot.yml file will be removed soon. Please use the code owners file to specify reviewers for Dependabot PRs. % --- .github/dependabot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95a8d66..aac594e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,7 +15,6 @@ updates: commit-message: prefix: "ci" assignees: [ "php-coder" ] - reviewers: [ "php-coder" ] labels: [ "kind/dependency-update", "area/ci" ] # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#rebase-strategy rebase-strategy: "disabled" @@ -35,7 +34,6 @@ updates: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy versioning-strategy: "increase" assignees: [ "php-coder" ] - reviewers: [ "php-coder" ] labels: [ "kind/dependency-update" ] # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#rebase-strategy rebase-strategy: "disabled" From 99823cfbbff383d9a833d3c158eb9073afefebc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 23:43:53 +0000 Subject: [PATCH 341/346] ci: bump jdx/mise-action from 2.2.2 to 2.2.3 Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2.2.2 to 2.2.3. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2.2.2...v2.2.3) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: 2.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 10b222b..637e227 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: run: docker compose ps - name: Install mise to install Hurl - uses: jdx/mise-action@v2.2.2 # https://github.com/jdx/mise-action + uses: jdx/mise-action@v2.2.3 # https://github.com/jdx/mise-action with: version: 2025.5.3 # [default: latest] mise version to install install: true # [default: true] run `mise install` From 2cbf3649717231867f263a23e5877fd3dd569565 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Wed, 28 May 2025 10:32:59 +0700 Subject: [PATCH 342/346] ci: update mise to 2025.5.14 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.5.4 - https://github.com/jdx/mise/releases/tag/v2025.5.5 - https://github.com/jdx/mise/releases/tag/v2025.5.6 - https://github.com/jdx/mise/releases/tag/v2025.5.7 - https://github.com/jdx/mise/releases/tag/v2025.5.8 - https://github.com/jdx/mise/releases/tag/v2025.5.9 - https://github.com/jdx/mise/releases/tag/v2025.5.10 - https://github.com/jdx/mise/releases/tag/v2025.5.11 - https://github.com/jdx/mise/releases/tag/v2025.5.12 - https://github.com/jdx/mise/releases/tag/v2025.5.13 - https://github.com/jdx/mise/releases/tag/v2025.5.14 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 637e227..c8206a9 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.2.3 # https://github.com/jdx/mise-action with: - version: 2025.5.3 # [default: latest] mise version to install + version: 2025.5.14 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From 7d68db7826738006688f7dde26349222464a5219 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:46:25 +0000 Subject: [PATCH 343/346] ci: bump jdx/mise-action from 2.2.3 to 2.3.1 Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2.2.3 to 2.3.1. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2.2.3...v2.3.1) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: 2.3.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index c8206a9..873de68 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: run: docker compose ps - name: Install mise to install Hurl - uses: jdx/mise-action@v2.2.3 # https://github.com/jdx/mise-action + uses: jdx/mise-action@v2.3.1 # https://github.com/jdx/mise-action with: version: 2025.5.14 # [default: latest] mise version to install install: true # [default: true] run `mise install` From 21e6787f787530808cce0dde07db450d298355f7 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Sun, 29 Jun 2025 21:49:20 +0700 Subject: [PATCH 344/346] ci: update mise to 2025.6.8 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.5.15 - https://github.com/jdx/mise/releases/tag/v2025.5.16 - https://github.com/jdx/mise/releases/tag/v2025.5.17 - https://github.com/jdx/mise/releases/tag/v2025.6.0 - https://github.com/jdx/mise/releases/tag/v2025.6.1 - https://github.com/jdx/mise/releases/tag/v2025.6.2 - https://github.com/jdx/mise/releases/tag/v2025.6.3 - https://github.com/jdx/mise/releases/tag/v2025.6.4 - https://github.com/jdx/mise/releases/tag/v2025.6.5 - https://github.com/jdx/mise/releases/tag/v2025.6.6 - https://github.com/jdx/mise/releases/tag/v2025.6.7 - https://github.com/jdx/mise/releases/tag/v2025.6.8 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 873de68..8654e85 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.3.1 # https://github.com/jdx/mise-action with: - version: 2025.5.14 # [default: latest] mise version to install + version: 2025.6.8 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From bc36b099bfa294201e62ece8051ed2c4ef4e6457 Mon Sep 17 00:00:00 2001 From: Slava Semushin Date: Mon, 14 Jul 2025 11:18:30 +0700 Subject: [PATCH 345/346] ci: update mise to 2025.7.8 Changelogs: - https://github.com/jdx/mise/releases/tag/v2025.7.0 - https://github.com/jdx/mise/releases/tag/v2025.7.1 - https://github.com/jdx/mise/releases/tag/v2025.7.2 - https://github.com/jdx/mise/releases/tag/v2025.7.3 - https://github.com/jdx/mise/releases/tag/v2025.7.4 - https://github.com/jdx/mise/releases/tag/v2025.7.7 - https://github.com/jdx/mise/releases/tag/v2025.7.8 --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8654e85..6a04f08 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,7 @@ jobs: - name: Install mise to install Hurl uses: jdx/mise-action@v2.3.1 # https://github.com/jdx/mise-action with: - version: 2025.6.8 # [default: latest] mise version to install + version: 2025.7.8 # [default: latest] mise version to install install: true # [default: true] run `mise install` cache: true # [default: true] cache mise using GitHub's cache log_level: info # [default: info] log level From 5b8ca66a05255c37724c67e8eef6598019aefad1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:45:53 +0000 Subject: [PATCH 346/346] ci: bump jdx/mise-action from 2.3.1 to 2.4.0 Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2.3.1 to 2.4.0. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2.3.1...v2.4.0) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6a04f08..2fe8a70 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -83,7 +83,7 @@ jobs: run: docker compose ps - name: Install mise to install Hurl - uses: jdx/mise-action@v2.3.1 # https://github.com/jdx/mise-action + uses: jdx/mise-action@v2.4.0 # https://github.com/jdx/mise-action with: version: 2025.7.8 # [default: latest] mise version to install install: true # [default: true] run `mise install`