@@ -4,6 +4,9 @@ import psycopg2.extras
4
4
<% # https: // fastapi.tiangolo.com/reference/status/ -%>
5
5
from fastapi import APIRouter, Depends, HTTPException, status
6
6
7
+ < % # LATER : add only when POST / PUT endpoints are present - %>
8
+ from pydantic import BaseModel
9
+
7
10
from db import db_connection
8
11
9
12
router = APIRouter()
@@ -38,7 +41,119 @@ function formatQueryForPython(query, indentLevel) {
38
41
return ` "${ sql} "`
39
42
}
40
43
44
+ // {'values':
45
+ // [
46
+ // {
47
+ // type: 'expr_list',
48
+ // value: [ { type: 'param', value: 'user_id' } ]
49
+ // }
50
+ // ]
51
+ // } => [ 'user_id' ]
52
+ function extractInsertValues (queryAst ) {
53
+ const values = queryAst .values .flatMap (elem => elem .value )
54
+ .map (elem => elem .type === ' param' ? elem .value : null )
55
+ .filter (elem => elem); // filter out nulls
56
+ return Array .from (new Set (values));
57
+ }
58
+
59
+ // LATER: reduce duplication with routes.go.ejs
60
+ function extractProperties (queryAst ) {
61
+ if (queryAst .type === ' insert' ) {
62
+ return extractInsertValues (queryAst);
63
+ }
64
+ return [];
65
+ }
66
+
67
+ // LATER: try to reduce duplication with routes.go.ejs
68
+ function findOutType (fieldsInfo , fieldName ) {
69
+ const defaultType = ' str' ;
70
+ const hasTypeInfo = fieldsInfo .hasOwnProperty (fieldName) && fieldsInfo[fieldName].hasOwnProperty (' type' );
71
+ if (hasTypeInfo && fieldsInfo[fieldName].type === ' integer' ) {
72
+ return ' int' ;
73
+ }
74
+ return defaultType;
75
+ }
76
+
77
+ // LATER: reduce duplication with routes.go.ejs
78
+ function addTypes (props , fieldsInfo ) {
79
+ return props .map (prop => {
80
+ return {
81
+ " name" : prop,
82
+ " type" : findOutType (fieldsInfo, prop),
83
+ }
84
+ });
85
+ }
86
+
87
+ // LATER: reduce duplication with routes.go.ejs
88
+ function query2dto (parser , method ) {
89
+ const query = removePlaceholders (method .query );
90
+ const queryAst = parser .astify (query);
91
+ const props = extractProperties (queryAst);
92
+ if (props .length === 0 ) {
93
+ console .warn (' Could not create DTO for query:' , formatQuery (query));
94
+ console .debug (' Query AST:' );
95
+ console .debug (queryAst);
96
+ return null ;
97
+ }
98
+ const fieldsInfo = method .dto && method .dto .fields ? method .dto .fields : {};
99
+ const propsWithTypes = addTypes (props, fieldsInfo);
100
+ const hasName = method .dto && method .dto .name && method .dto .name .length > 0 ;
101
+ const name = hasName ? method .dto .name : " Dto" + ++ globalDtoCounter;
102
+ return {
103
+ " name" : name,
104
+ " hasUserProvidedName" : hasName,
105
+ " props" : propsWithTypes,
106
+ // required for de-duplication
107
+ // [ {name:foo, type:int}, {name:bar, type:string} ] => "foo=int bar=string"
108
+ // LATER: sort before join
109
+ " signature" : propsWithTypes .map (field => ` ${ field .name } =${ field .type } ` ).join (' ' )
110
+ };
111
+ }
112
+
113
+ // https://fastapi.tiangolo.com/tutorial/body/
114
+ function dto2model (dto ) {
115
+ let result = ` class ${ dto .name } (BaseModel):\n ` ;
116
+ dto .props .forEach (prop => {
117
+ result += ` ${ prop .name } : ${ prop .type } \n `
118
+ });
119
+ return result;
120
+ }
121
+
122
+ let globalDtoCounter = 0 ;
123
+ const dtoCache = {};
124
+
125
+ // LATER: reduce duplication with routes.go.ejs
126
+ function cacheDto (dto ) {
127
+ dtoCache[dto .signature ] = dto .name ;
128
+ return dto;
129
+ }
130
+
131
+ // LATER: reduce duplication with routes.go.ejs
132
+ function dtoInCache (dto ) {
133
+ // always prefer user specified name even when we have a similar DTO in cache
134
+ if (dto .hasUserProvidedName ) {
135
+ return false ;
136
+ }
137
+ return dtoCache .hasOwnProperty (dto .signature );
138
+ }
41
139
140
+ // Generate models
141
+ const verbs_with_dto = [ ' post' ]
142
+ endpoints .forEach (function (endpoint ) {
143
+ const dtos = endpoint .methods
144
+ .filter (method => verbs_with_dto .includes (method .verb ))
145
+ .map (method => query2dto (sqlParser, method))
146
+ .filter (elem => elem) // filter out nulls
147
+ .filter (dto => ! dtoInCache (dto))
148
+ .map (dto => dto2model (cacheDto (dto)))
149
+ .forEach (model => {
150
+ % >
151
+ < %- model -% >
152
+ < %
153
+ });
154
+ });
155
+
156
+ // Generate endpoints
42
157
endpoints .forEach (function (endpoint ) {
43
158
const path = convertToFastApiPath (endpoint .path )
44
159
const argsFromPath = extractParamsFromPath (endpoint .path )
@@ -119,10 +234,14 @@ def <%- pythonMethodName %>(<%- methodArgs.join(', ') %>):
119
234
< %
120
235
}
121
236
if (method .name === ' post' ) {
237
+ const dto = query2dto (sqlParser, method);
238
+ // LATER: do we really need signature and cache?
239
+ const cacheKey = dto ? dto .signature : null ;
240
+ const model = dtoInCache (dto) ? dtoCache[cacheKey] : dto .name ;
122
241
% >
123
242
124
243
@router .post (' <%- path %>' , status_code = status .HTTP_204_NO_CONTENT )
125
- def < %- pythonMethodName % > ():
244
+ def < %- pythonMethodName % > (payload : < %- model % > ):
126
245
pass
127
246
< %
128
247
0 commit comments