-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.py
249 lines (179 loc) · 7.89 KB
/
config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# -*- coding: utf-8 -*-
from typing import List, Dict, Optional
from pydantic import BaseModel, Field
from utils import snake_to_title_case, snake_to_camel_case, database_data_type, graphql_data_type, python_type_hint
class DataModelGraphQLRelation(BaseModel):
# name of the datamodel relationship that the relation operates via, see DataModel.relationships
relationship: str
# if this is a many-to-one or one-to-many relationship - FIXME many-to-many not supported yet
as_list: bool = True
# description for documention
description: Optional[str]
# whether this relation should be eager loaded by default when pulling data - FIXME this should eagerload when needed
eager_load: bool = False
class DataModelRelationship(BaseModel):
# sqlalchemy backref option, to specify that the relationship can be reversed
backref: str
# the name of the config model (not database model) that this relationship refers to
foreign_model: str
# the field that this model should join on - must be a foreign key
foreign_model_field: str
@property
def foreign_database_model_name(self):
return snake_to_title_case(self.foreign_model)
class DataModelFieldFilter(BaseModel):
# whether this field can be specified and used as "equals"
eq: bool = False
ne: bool = False
like: bool = False
re: bool = False
lt: bool = False
gt: bool = False
lte: bool = False
gte: bool = False
_known_filters = [ 'eq', 'ne', 'like', 're', 'lt', 'gt', 'lte', 'gte' ]
class DataModelField(BaseModel):
# field name in the model, e.g. username
name: str
# field type, e.g. string, integer, bigint, smallint, datetime, date, decimal, numeric,
type: str
# default value, if not specified, callable (e.g. datetime.utcnow) or value with quotes (e.g. "foo")
default: Optional[str]
# sqlalchemy onupdate value, callable, e.g. datatime.utcnow
onupdate: Optional[str]
# whether this field is nullable, defaults to false
nullable: bool = Field(False)
# whether this field is a foreign key or not, this specifies the name of the config model NOT the table name
foreign_model: Optional[str]
# and this is the field name within that model
foreign_model_field: Optional[str]
# can this field be specified within a create api method?
can_create: bool = Field(True)
# can this field be specified within an update api method?
can_update: bool = Field(True)
# can this field be specified within a patch api method?
can_patch: bool = Field(True)
# field description to be included in docs
description: Optional[str]
# filter information that can be specified when retrieving via the api
filters: Optional[DataModelFieldFilter] = DataModelFieldFilter()
def foreign_key(self, config):
key = None
if self.foreign_model and self.foreign_model_field:
key = f"{config.datamodel[self.foreign_model].table}.{self.foreign_model_field}"
return key
@property
def python_type_hint(self):
return python_type_hint(self.type)
@property
def database_field_name(self):
return self.name
@property
def database_field_type(self):
return database_data_type(self.type)
@property
def graphql_field_name(self):
return snake_to_camel_case(self.name)
@property
def graphql_type_name(self):
return graphql_data_type(self.type)
class DataModelGraphQL(BaseModel):
# should we generate a patch api method?
patch: bool = Field(True)
# should we generate a create api method?
create: bool = Field(True)
# should we generate a delete api method?
delete: bool = Field(True)
# should we generate an update api method?
update: bool = Field(True)
# specify name for patch mutation
patchMutation: Optional[str] = None
# specify name for create mutation
createMutation: Optional[str] = None
# specify name for delete mutation
deleteMutation: Optional[str] = None
# specify name for update mutation
updateMutation: Optional[str] = None
# extra properties that should be exposed via relationships
hierarchy: Dict[str, DataModelGraphQLRelation] = Field(None)
# name of the identifier that should be used in graphql
# identifier: Optional[str]
class DataModelIndex(BaseModel):
# list of fields to be included in this index
fields: List[str]
class DataModel(BaseModel):
# name of the table to be created in the database for this model
table: str
# name of the primary key field (FIXME could be handled in model fields)
primary_key: str
graphql: DataModelGraphQL = Field(DataModelGraphQL())
fields: List[DataModelField]
# mapping of internal relationship name to relationship config
relationships: Optional[Dict[str, DataModelRelationship]]
# mapping of index name to index config
indexes: Optional[Dict[str, DataModelIndex]]
# description of this model to be added to the schema
description: Optional[str]
# private
backrefs: Dict[str, DataModelRelationship] = None
@property
def graphql_patch_mutation(self):
return self.graphql.patchMutation or f"patch{self.graphql_type_name}"
@property
def graphql_create_mutation(self):
return self.graphql.createMutation or f"create{self.graphql_type_name}"
@property
def graphql_delete_mutation(self):
return self.graphql.deleteMutation or f"delete{self.graphql_type_name}"
@property
def graphql_update_mutation(self):
return self.graphql.updateMutation or f"update{self.graphql_type_name}"
@property
def database_model_name(self):
return snake_to_title_case(self.table)
@property
def graphql_identifier_type(self):
return "Int" # FIXME not necessarily
@property
def graphql_identifier(self):
# return self.graphql.identifier or snake_to_camel_case(self.primary_key)
return snake_to_camel_case(self.primary_key)
@property
def graphql_type_name(self):
return snake_to_title_case(self.table)
class Backend(BaseModel):
# package name for templates - TODO remove this in the future
package: str = Field("internal")
# list of import statements that should be added to the generated model files
model_imports: Optional[List[str]]
class Config(BaseModel):
backend: Backend
datamodel: Dict[str, DataModel]
backrefs_populated: bool = False
def _populate_datamodelbackrefs(self):
if not self.backrefs_populated:
self.backrefs_populated = True
for datamodel_name, datamodel in self.datamodel.items():
if datamodel.relationships:
for rel_name, relationship in datamodel.relationships.items():
if relationship.backref:
foreign_model = self.datamodel[relationship.foreign_model]
if foreign_model.backrefs is None:
foreign_model.backrefs = {}
foreign_model.backrefs[relationship.backref] = DataModelRelationship(
backref=rel_name,
foreign_model=datamodel_name,
foreign_model_field=relationship.foreign_model_field,
)
def graphql_relation_foreign_model(
self,
datamodel: DataModel,
relation: DataModelGraphQLRelation,
) -> DataModel:
self._populate_datamodelbackrefs()
if datamodel.relationships and relation.relationship in datamodel.relationships:
relationship = datamodel.relationships[relation.relationship]
elif datamodel.backrefs and relation.relationship in datamodel.backrefs:
relationship = datamodel.backrefs[relation.relationship]
foreign_model = self.datamodel[relationship.foreign_model]
return foreign_model