sera-2 1.2.0__py3-none-any.whl → 1.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sera/constants.py +4 -0
- sera/libs/base_orm.py +1 -1
- sera/libs/base_service.py +44 -8
- sera/make/__main__.py +14 -1
- sera/make/make_app.py +46 -16
- sera/make/make_python_api.py +63 -44
- sera/make/make_python_model.py +262 -120
- sera/make/make_typescript_model.py +846 -0
- sera/misc/__init__.py +4 -0
- sera/misc/_utils.py +12 -0
- sera/models/__init__.py +3 -1
- sera/models/_class.py +18 -1
- sera/models/_collection.py +10 -2
- sera/models/_datatype.py +150 -45
- sera/models/_module.py +37 -6
- sera/models/_parse.py +69 -18
- sera/models/_property.py +27 -3
- sera/typing.py +3 -0
- {sera_2-1.2.0.dist-info → sera_2-1.4.2.dist-info}/METADATA +3 -3
- sera_2-1.4.2.dist-info/RECORD +30 -0
- {sera_2-1.2.0.dist-info → sera_2-1.4.2.dist-info}/WHEEL +1 -1
- sera/.DS_Store +0 -0
- sera/make/.DS_Store +0 -0
- sera_2-1.2.0.dist-info/RECORD +0 -31
@@ -1 +1,847 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any, Callable
|
4
|
+
|
5
|
+
from codegen.models import AST, PredefinedFn, Program, expr, stmt
|
6
|
+
from codegen.models.var import DeferredVar
|
7
|
+
from loguru import logger
|
8
|
+
from sera.misc import (
|
9
|
+
assert_isinstance,
|
10
|
+
assert_not_null,
|
11
|
+
to_camel_case,
|
12
|
+
to_pascal_case,
|
13
|
+
to_snake_case,
|
14
|
+
)
|
15
|
+
from sera.models import (
|
16
|
+
Class,
|
17
|
+
DataProperty,
|
18
|
+
ObjectProperty,
|
19
|
+
Package,
|
20
|
+
Schema,
|
21
|
+
TsTypeWithDep,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
26
|
+
"""Generate TypeScript data model from the schema. The data model aligns with the public data model in Python, not the database model."""
|
27
|
+
app = target_pkg.app
|
28
|
+
|
29
|
+
def clone_prop(prop: DataProperty | ObjectProperty, value: expr.Expr):
|
30
|
+
# detect all complex types is hard, we can assume that any update to this does not mutate
|
31
|
+
# the original object, then it's okay.
|
32
|
+
return value
|
33
|
+
|
34
|
+
def make_normal(cls: Class, pkg: Package):
|
35
|
+
"""Make a data model for the normal Python data model"""
|
36
|
+
if not cls.is_public:
|
37
|
+
# skip classes that are not public
|
38
|
+
return
|
39
|
+
|
40
|
+
idprop = cls.get_id_property()
|
41
|
+
|
42
|
+
program = Program()
|
43
|
+
|
44
|
+
prop_defs = []
|
45
|
+
prop_constructor_assigns = []
|
46
|
+
deser_args = []
|
47
|
+
|
48
|
+
for prop in cls.properties.values():
|
49
|
+
if prop.data.is_private:
|
50
|
+
# skip private fields as this is for APIs exchange
|
51
|
+
continue
|
52
|
+
|
53
|
+
propname = to_camel_case(prop.name)
|
54
|
+
|
55
|
+
if isinstance(prop, DataProperty):
|
56
|
+
tstype = prop.get_data_model_datatype().get_typescript_type()
|
57
|
+
if tstype.dep is not None:
|
58
|
+
program.import_(tstype.dep, True)
|
59
|
+
|
60
|
+
if idprop is not None and prop.name == idprop.name:
|
61
|
+
# use id type alias
|
62
|
+
tstype = TsTypeWithDep(f"{cls.name}Id")
|
63
|
+
|
64
|
+
deser_args.append(
|
65
|
+
(
|
66
|
+
expr.ExprIdent(propname),
|
67
|
+
PredefinedFn.attr_getter(
|
68
|
+
expr.ExprIdent("data"), expr.ExprIdent(prop.name)
|
69
|
+
),
|
70
|
+
)
|
71
|
+
)
|
72
|
+
else:
|
73
|
+
assert isinstance(prop, ObjectProperty)
|
74
|
+
if prop.target.db is not None:
|
75
|
+
# this class is stored in the database, we store the id instead
|
76
|
+
tstype = TsTypeWithDep(
|
77
|
+
f"{prop.target.name}Id",
|
78
|
+
f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id",
|
79
|
+
)
|
80
|
+
if prop.cardinality.is_star_to_many():
|
81
|
+
tstype = tstype.as_list_type()
|
82
|
+
deser_args.append(
|
83
|
+
(
|
84
|
+
expr.ExprIdent(propname),
|
85
|
+
PredefinedFn.attr_getter(
|
86
|
+
expr.ExprIdent("data"), expr.ExprIdent(prop.name)
|
87
|
+
),
|
88
|
+
)
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
# we are going to store the whole object
|
92
|
+
tstype = TsTypeWithDep(
|
93
|
+
prop.target.name,
|
94
|
+
f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}",
|
95
|
+
)
|
96
|
+
if prop.cardinality.is_star_to_many():
|
97
|
+
tstype = tstype.as_list_type()
|
98
|
+
deser_args.append(
|
99
|
+
(
|
100
|
+
expr.ExprIdent(propname),
|
101
|
+
PredefinedFn.map_list(
|
102
|
+
PredefinedFn.attr_getter(
|
103
|
+
expr.ExprIdent("data"),
|
104
|
+
expr.ExprIdent(prop.name),
|
105
|
+
),
|
106
|
+
lambda item: expr.ExprMethodCall(
|
107
|
+
expr.ExprIdent(
|
108
|
+
assert_isinstance(
|
109
|
+
prop, ObjectProperty
|
110
|
+
).target.name
|
111
|
+
),
|
112
|
+
"deser",
|
113
|
+
[item],
|
114
|
+
),
|
115
|
+
),
|
116
|
+
)
|
117
|
+
)
|
118
|
+
else:
|
119
|
+
deser_args.append(
|
120
|
+
(
|
121
|
+
expr.ExprIdent(propname),
|
122
|
+
expr.ExprFuncCall(
|
123
|
+
PredefinedFn.attr_getter(
|
124
|
+
expr.ExprIdent(prop.target.name),
|
125
|
+
expr.ExprIdent("deser"),
|
126
|
+
),
|
127
|
+
[
|
128
|
+
PredefinedFn.attr_getter(
|
129
|
+
expr.ExprIdent("data"),
|
130
|
+
expr.ExprIdent(prop.name),
|
131
|
+
)
|
132
|
+
],
|
133
|
+
),
|
134
|
+
)
|
135
|
+
)
|
136
|
+
|
137
|
+
if tstype.dep is not None:
|
138
|
+
program.import_(
|
139
|
+
tstype.dep,
|
140
|
+
True,
|
141
|
+
)
|
142
|
+
|
143
|
+
prop_defs.append(stmt.DefClassVarStatement(propname, tstype.type))
|
144
|
+
prop_constructor_assigns.append(
|
145
|
+
stmt.AssignStatement(
|
146
|
+
PredefinedFn.attr_getter(
|
147
|
+
expr.ExprIdent("this"),
|
148
|
+
expr.ExprIdent(propname),
|
149
|
+
),
|
150
|
+
expr.ExprIdent("args." + propname),
|
151
|
+
)
|
152
|
+
)
|
153
|
+
|
154
|
+
program.root(
|
155
|
+
stmt.LineBreak(),
|
156
|
+
(
|
157
|
+
stmt.TypescriptStatement(
|
158
|
+
f"export type {cls.name}Id = {idprop.get_data_model_datatype().get_typescript_type().type};"
|
159
|
+
)
|
160
|
+
if idprop is not None
|
161
|
+
else None
|
162
|
+
),
|
163
|
+
stmt.LineBreak(),
|
164
|
+
lambda ast00: ast00.interface(
|
165
|
+
cls.name + "ConstructorArgs",
|
166
|
+
)(*prop_defs),
|
167
|
+
stmt.LineBreak(),
|
168
|
+
lambda ast10: ast10.class_(cls.name)(
|
169
|
+
*prop_defs,
|
170
|
+
stmt.LineBreak(),
|
171
|
+
lambda ast11: ast11.func(
|
172
|
+
"constructor",
|
173
|
+
[
|
174
|
+
DeferredVar.simple(
|
175
|
+
"args", expr.ExprIdent(cls.name + "ConstructorArgs")
|
176
|
+
),
|
177
|
+
],
|
178
|
+
)(*prop_constructor_assigns),
|
179
|
+
stmt.LineBreak(),
|
180
|
+
lambda ast12: ast12.func(
|
181
|
+
"deser",
|
182
|
+
[
|
183
|
+
DeferredVar.simple("data", expr.ExprIdent("any")),
|
184
|
+
],
|
185
|
+
expr.ExprIdent(cls.name),
|
186
|
+
is_static=True,
|
187
|
+
comment="Deserialize the data from the server to create a new instance of the class",
|
188
|
+
)(
|
189
|
+
lambda ast: ast.return_(
|
190
|
+
expr.ExprNewInstance(
|
191
|
+
expr.ExprIdent(cls.name), [PredefinedFn.dict(deser_args)]
|
192
|
+
)
|
193
|
+
)
|
194
|
+
),
|
195
|
+
),
|
196
|
+
)
|
197
|
+
|
198
|
+
pkg.module(cls.name).write(program)
|
199
|
+
|
200
|
+
def make_draft(cls: Class, pkg: Package):
|
201
|
+
if not cls.is_public:
|
202
|
+
# skip classes that are not public
|
203
|
+
return
|
204
|
+
|
205
|
+
idprop = cls.get_id_property()
|
206
|
+
|
207
|
+
draft_clsname = "Draft" + cls.name
|
208
|
+
|
209
|
+
program = Program()
|
210
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
211
|
+
program.import_("mobx.makeObservable", True)
|
212
|
+
program.import_("mobx.observable", True)
|
213
|
+
program.import_("mobx.action", True)
|
214
|
+
|
215
|
+
program.root.linebreak()
|
216
|
+
|
217
|
+
# make sure that the property stale is not in existing properties
|
218
|
+
if "stale" in cls.properties:
|
219
|
+
raise ValueError(f"Class {cls.name} already has property stale")
|
220
|
+
|
221
|
+
# information about class primary key
|
222
|
+
cls_pk = None
|
223
|
+
observable_args: list[tuple[expr.Expr, expr.ExprIdent]] = []
|
224
|
+
prop_defs = []
|
225
|
+
prop_constructor_assigns = []
|
226
|
+
# attrs needed for the cls.create function
|
227
|
+
create_args = []
|
228
|
+
update_args = []
|
229
|
+
ser_args = []
|
230
|
+
update_field_funcs: list[Callable[[AST], Any]] = []
|
231
|
+
|
232
|
+
for prop in cls.properties.values():
|
233
|
+
if prop.data.is_private:
|
234
|
+
# skip private fields as this is for APIs exchange
|
235
|
+
continue
|
236
|
+
|
237
|
+
propname = to_camel_case(prop.name)
|
238
|
+
|
239
|
+
def _update_field_func(
|
240
|
+
prop: DataProperty | ObjectProperty,
|
241
|
+
propname: str,
|
242
|
+
tstype: TsTypeWithDep,
|
243
|
+
draft_clsname: str,
|
244
|
+
):
|
245
|
+
return lambda ast: ast(
|
246
|
+
stmt.LineBreak(),
|
247
|
+
lambda ast01: ast01.func(
|
248
|
+
f"update{to_pascal_case(prop.name)}",
|
249
|
+
[
|
250
|
+
DeferredVar.simple(
|
251
|
+
"value",
|
252
|
+
expr.ExprIdent(tstype.type),
|
253
|
+
),
|
254
|
+
],
|
255
|
+
expr.ExprIdent(draft_clsname),
|
256
|
+
comment=f"Update the `{prop.name}` field",
|
257
|
+
)(
|
258
|
+
stmt.AssignStatement(
|
259
|
+
PredefinedFn.attr_getter(
|
260
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
261
|
+
),
|
262
|
+
expr.ExprIdent("value"),
|
263
|
+
),
|
264
|
+
stmt.AssignStatement(
|
265
|
+
PredefinedFn.attr_getter(
|
266
|
+
expr.ExprIdent("this"), expr.ExprIdent("stale")
|
267
|
+
),
|
268
|
+
expr.ExprConstant(True),
|
269
|
+
),
|
270
|
+
stmt.ReturnStatement(expr.ExprIdent("this")),
|
271
|
+
),
|
272
|
+
)
|
273
|
+
|
274
|
+
if isinstance(prop, DataProperty):
|
275
|
+
tstype = prop.get_data_model_datatype().get_typescript_type()
|
276
|
+
if idprop is not None and prop.name == idprop.name:
|
277
|
+
# use id type alias
|
278
|
+
tstype = TsTypeWithDep(
|
279
|
+
f"{cls.name}Id",
|
280
|
+
dep=f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id",
|
281
|
+
)
|
282
|
+
if tstype.dep is not None:
|
283
|
+
program.import_(tstype.dep, True)
|
284
|
+
|
285
|
+
# however, if this is a primary key and auto-increment, we set a different default value
|
286
|
+
# to be -1 to avoid start from 0
|
287
|
+
if (
|
288
|
+
prop.db is not None
|
289
|
+
and prop.db.is_primary_key
|
290
|
+
and prop.db.is_auto_increment
|
291
|
+
):
|
292
|
+
create_propvalue = expr.ExprConstant(-1)
|
293
|
+
else:
|
294
|
+
create_propvalue = tstype.get_default()
|
295
|
+
|
296
|
+
if prop.db is not None and prop.db.is_primary_key:
|
297
|
+
# for checking if the primary key is from the database or default (create_propvalue)
|
298
|
+
cls_pk = (expr.ExprIdent(propname), create_propvalue)
|
299
|
+
|
300
|
+
update_propvalue = PredefinedFn.attr_getter(
|
301
|
+
expr.ExprIdent("record"), expr.ExprIdent(propname)
|
302
|
+
)
|
303
|
+
|
304
|
+
ser_args.append(
|
305
|
+
(
|
306
|
+
expr.ExprIdent(prop.name),
|
307
|
+
PredefinedFn.attr_getter(
|
308
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
309
|
+
),
|
310
|
+
)
|
311
|
+
)
|
312
|
+
if not (prop.db is not None and prop.db.is_primary_key):
|
313
|
+
# skip observable for primary key as it is not needed
|
314
|
+
observable_args.append(
|
315
|
+
(
|
316
|
+
expr.ExprIdent(propname),
|
317
|
+
expr.ExprIdent("observable"),
|
318
|
+
)
|
319
|
+
)
|
320
|
+
observable_args.append(
|
321
|
+
(
|
322
|
+
expr.ExprIdent(f"update{to_pascal_case(prop.name)}"),
|
323
|
+
expr.ExprIdent("action"),
|
324
|
+
)
|
325
|
+
)
|
326
|
+
else:
|
327
|
+
assert isinstance(prop, ObjectProperty)
|
328
|
+
if prop.target.db is not None:
|
329
|
+
# this class is stored in the database, we store the id instead
|
330
|
+
tstype = TsTypeWithDep(
|
331
|
+
f"{prop.target.name}Id",
|
332
|
+
f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id",
|
333
|
+
)
|
334
|
+
if prop.cardinality.is_star_to_many():
|
335
|
+
tstype = tstype.as_list_type()
|
336
|
+
create_propvalue = tstype.get_default()
|
337
|
+
update_propvalue = PredefinedFn.attr_getter(
|
338
|
+
expr.ExprIdent("record"), expr.ExprIdent(propname)
|
339
|
+
)
|
340
|
+
ser_args.append(
|
341
|
+
(
|
342
|
+
expr.ExprIdent(prop.name),
|
343
|
+
PredefinedFn.attr_getter(
|
344
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
345
|
+
),
|
346
|
+
)
|
347
|
+
)
|
348
|
+
else:
|
349
|
+
# we are going to store the whole object
|
350
|
+
tstype = TsTypeWithDep(
|
351
|
+
f"Draft{prop.target.name}",
|
352
|
+
f"@.models.{prop.target.get_tsmodule_name()}.Draft{prop.target.name}.Draft{prop.target.name}",
|
353
|
+
)
|
354
|
+
if prop.cardinality.is_star_to_many():
|
355
|
+
tstype = tstype.as_list_type()
|
356
|
+
create_propvalue = expr.ExprConstant([])
|
357
|
+
update_propvalue = PredefinedFn.map_list(
|
358
|
+
PredefinedFn.attr_getter(
|
359
|
+
expr.ExprIdent("record"), expr.ExprIdent(propname)
|
360
|
+
),
|
361
|
+
lambda item: expr.ExprMethodCall(
|
362
|
+
expr.ExprIdent(tstype.type),
|
363
|
+
"update",
|
364
|
+
[item],
|
365
|
+
),
|
366
|
+
)
|
367
|
+
ser_args.append(
|
368
|
+
(
|
369
|
+
expr.ExprIdent(prop.name),
|
370
|
+
PredefinedFn.map_list(
|
371
|
+
PredefinedFn.attr_getter(
|
372
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
373
|
+
),
|
374
|
+
lambda item: expr.ExprMethodCall(item, "ser", []),
|
375
|
+
),
|
376
|
+
)
|
377
|
+
)
|
378
|
+
else:
|
379
|
+
create_propvalue = expr.ExprMethodCall(
|
380
|
+
expr.ExprIdent(tstype.type),
|
381
|
+
"create",
|
382
|
+
[],
|
383
|
+
)
|
384
|
+
update_propvalue = expr.ExprMethodCall(
|
385
|
+
expr.ExprIdent(tstype.type),
|
386
|
+
"update",
|
387
|
+
[
|
388
|
+
PredefinedFn.attr_getter(
|
389
|
+
expr.ExprIdent("record"), expr.ExprIdent(propname)
|
390
|
+
),
|
391
|
+
],
|
392
|
+
)
|
393
|
+
ser_args.append(
|
394
|
+
(
|
395
|
+
expr.ExprIdent(prop.name),
|
396
|
+
expr.ExprMethodCall(
|
397
|
+
PredefinedFn.attr_getter(
|
398
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
399
|
+
),
|
400
|
+
"ser",
|
401
|
+
[],
|
402
|
+
),
|
403
|
+
)
|
404
|
+
)
|
405
|
+
|
406
|
+
if tstype.dep is not None:
|
407
|
+
program.import_(
|
408
|
+
tstype.dep,
|
409
|
+
True,
|
410
|
+
)
|
411
|
+
|
412
|
+
observable_args.append(
|
413
|
+
(
|
414
|
+
expr.ExprIdent(propname),
|
415
|
+
expr.ExprIdent("observable"),
|
416
|
+
)
|
417
|
+
)
|
418
|
+
observable_args.append(
|
419
|
+
(
|
420
|
+
expr.ExprIdent(f"update{to_pascal_case(prop.name)}"),
|
421
|
+
expr.ExprIdent("action"),
|
422
|
+
)
|
423
|
+
)
|
424
|
+
|
425
|
+
prop_defs.append(stmt.DefClassVarStatement(propname, tstype.type))
|
426
|
+
prop_constructor_assigns.append(
|
427
|
+
stmt.AssignStatement(
|
428
|
+
PredefinedFn.attr_getter(
|
429
|
+
expr.ExprIdent("this"),
|
430
|
+
expr.ExprIdent(propname),
|
431
|
+
),
|
432
|
+
expr.ExprIdent("args." + propname),
|
433
|
+
)
|
434
|
+
)
|
435
|
+
create_args.append((expr.ExprIdent(propname), create_propvalue))
|
436
|
+
update_args.append(
|
437
|
+
(
|
438
|
+
expr.ExprIdent(propname),
|
439
|
+
# if this is mutable property, we need to copy to make it immutable.
|
440
|
+
clone_prop(prop, update_propvalue),
|
441
|
+
)
|
442
|
+
)
|
443
|
+
update_field_funcs.append(
|
444
|
+
_update_field_func(prop, propname, tstype, draft_clsname)
|
445
|
+
)
|
446
|
+
|
447
|
+
prop_defs.append(stmt.DefClassVarStatement("stale", "boolean"))
|
448
|
+
prop_constructor_assigns.append(
|
449
|
+
stmt.AssignStatement(
|
450
|
+
PredefinedFn.attr_getter(
|
451
|
+
expr.ExprIdent("this"), expr.ExprIdent("stale")
|
452
|
+
),
|
453
|
+
expr.ExprIdent("args.stale"),
|
454
|
+
)
|
455
|
+
)
|
456
|
+
observable_args.append(
|
457
|
+
(
|
458
|
+
expr.ExprIdent("stale"),
|
459
|
+
expr.ExprIdent("observable"),
|
460
|
+
)
|
461
|
+
)
|
462
|
+
create_args.append(
|
463
|
+
(
|
464
|
+
expr.ExprIdent("stale"),
|
465
|
+
expr.ExprConstant(True),
|
466
|
+
),
|
467
|
+
)
|
468
|
+
update_args.append(
|
469
|
+
(
|
470
|
+
expr.ExprIdent("stale"),
|
471
|
+
expr.ExprConstant(False),
|
472
|
+
),
|
473
|
+
)
|
474
|
+
observable_args.sort(key=lambda x: {"observable": 0, "action": 1}[x[1].ident])
|
475
|
+
|
476
|
+
program.root(
|
477
|
+
lambda ast00: ast00.interface(
|
478
|
+
draft_clsname + "ConstructorArgs",
|
479
|
+
)(*prop_defs),
|
480
|
+
stmt.LineBreak(),
|
481
|
+
lambda ast10: ast10.class_(draft_clsname)(
|
482
|
+
*prop_defs,
|
483
|
+
stmt.LineBreak(),
|
484
|
+
lambda ast10: ast10.func(
|
485
|
+
"constructor",
|
486
|
+
[
|
487
|
+
DeferredVar.simple(
|
488
|
+
"args",
|
489
|
+
expr.ExprIdent(draft_clsname + "ConstructorArgs"),
|
490
|
+
),
|
491
|
+
],
|
492
|
+
)(
|
493
|
+
*prop_constructor_assigns,
|
494
|
+
stmt.LineBreak(),
|
495
|
+
stmt.SingleExprStatement(
|
496
|
+
expr.ExprFuncCall(
|
497
|
+
expr.ExprIdent("makeObservable"),
|
498
|
+
[
|
499
|
+
expr.ExprIdent("this"),
|
500
|
+
PredefinedFn.dict(observable_args),
|
501
|
+
],
|
502
|
+
)
|
503
|
+
),
|
504
|
+
),
|
505
|
+
stmt.LineBreak(),
|
506
|
+
lambda ast11: (
|
507
|
+
ast11.func(
|
508
|
+
"is_new_record",
|
509
|
+
[],
|
510
|
+
expr.ExprIdent("boolean"),
|
511
|
+
comment="Check if this draft is for creating a new record",
|
512
|
+
)(
|
513
|
+
stmt.ReturnStatement(
|
514
|
+
expr.ExprEqual(
|
515
|
+
PredefinedFn.attr_getter(
|
516
|
+
expr.ExprIdent("this"), cls_pk[0]
|
517
|
+
),
|
518
|
+
cls_pk[1],
|
519
|
+
)
|
520
|
+
)
|
521
|
+
)
|
522
|
+
if cls_pk is not None
|
523
|
+
else None
|
524
|
+
),
|
525
|
+
stmt.LineBreak(),
|
526
|
+
lambda ast12: ast12.func(
|
527
|
+
"create",
|
528
|
+
[],
|
529
|
+
expr.ExprIdent(draft_clsname),
|
530
|
+
is_static=True,
|
531
|
+
comment="Make a new draft for creating a new record",
|
532
|
+
)(
|
533
|
+
stmt.ReturnStatement(
|
534
|
+
expr.ExprNewInstance(
|
535
|
+
expr.ExprIdent(draft_clsname),
|
536
|
+
[PredefinedFn.dict(create_args)],
|
537
|
+
)
|
538
|
+
),
|
539
|
+
),
|
540
|
+
stmt.LineBreak(),
|
541
|
+
lambda ast13: ast13.func(
|
542
|
+
"update",
|
543
|
+
[DeferredVar.simple("record", expr.ExprIdent(cls.name))],
|
544
|
+
expr.ExprIdent(draft_clsname),
|
545
|
+
is_static=True,
|
546
|
+
comment="Make a new draft for updating an existing record",
|
547
|
+
)(
|
548
|
+
stmt.ReturnStatement(
|
549
|
+
expr.ExprNewInstance(
|
550
|
+
expr.ExprIdent(draft_clsname),
|
551
|
+
[PredefinedFn.dict(update_args)],
|
552
|
+
)
|
553
|
+
),
|
554
|
+
),
|
555
|
+
*update_field_funcs,
|
556
|
+
stmt.LineBreak(),
|
557
|
+
lambda ast14: ast14.func(
|
558
|
+
"ser",
|
559
|
+
[],
|
560
|
+
expr.ExprIdent("any"),
|
561
|
+
comment="Serialize the draft to communicate with the server",
|
562
|
+
)(
|
563
|
+
stmt.ReturnStatement(
|
564
|
+
PredefinedFn.dict(ser_args),
|
565
|
+
),
|
566
|
+
),
|
567
|
+
),
|
568
|
+
)
|
569
|
+
|
570
|
+
pkg.module("Draft" + cls.name).write(program)
|
571
|
+
|
572
|
+
def make_table(cls: Class, pkg: Package):
|
573
|
+
if not cls.is_public or cls.db is None:
|
574
|
+
# skip classes that are not public and not stored in the database
|
575
|
+
return
|
576
|
+
|
577
|
+
outmod = pkg.module(cls.name + "Table")
|
578
|
+
if outmod.exists():
|
579
|
+
# skip if the module already exists
|
580
|
+
logger.info(f"Module {outmod.path} already exists, skip")
|
581
|
+
return
|
582
|
+
|
583
|
+
program = Program()
|
584
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
585
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id", True)
|
586
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}Query.query", True)
|
587
|
+
program.import_(
|
588
|
+
f"@.models.{pkg.dir.name}.Draft{cls.name}.Draft{cls.name}", True
|
589
|
+
)
|
590
|
+
program.import_("sera-db.Table", True)
|
591
|
+
program.import_("sera-db.DB", True)
|
592
|
+
|
593
|
+
program.root(
|
594
|
+
stmt.LineBreak(),
|
595
|
+
lambda ast00: ast00.class_(
|
596
|
+
f"{cls.name}Table",
|
597
|
+
[expr.ExprIdent(f"Table<{cls.name}Id, {cls.name}, Draft{cls.name}>")],
|
598
|
+
)(
|
599
|
+
lambda ast01: ast01.func(
|
600
|
+
"constructor",
|
601
|
+
[
|
602
|
+
DeferredVar.simple(
|
603
|
+
"db",
|
604
|
+
expr.ExprIdent("DB"),
|
605
|
+
)
|
606
|
+
],
|
607
|
+
)(
|
608
|
+
stmt.SingleExprStatement(
|
609
|
+
expr.ExprFuncCall(
|
610
|
+
expr.ExprIdent("super"),
|
611
|
+
[
|
612
|
+
PredefinedFn.dict(
|
613
|
+
[
|
614
|
+
(
|
615
|
+
expr.ExprIdent("cls"),
|
616
|
+
expr.ExprIdent(cls.name),
|
617
|
+
),
|
618
|
+
(
|
619
|
+
expr.ExprIdent("remoteURL"),
|
620
|
+
expr.ExprConstant(
|
621
|
+
f"/api/{to_snake_case(cls.name).replace('_', '-')}"
|
622
|
+
),
|
623
|
+
),
|
624
|
+
(
|
625
|
+
expr.ExprIdent("db"),
|
626
|
+
expr.ExprIdent("db"),
|
627
|
+
),
|
628
|
+
(
|
629
|
+
expr.ExprIdent("queryProcessor"),
|
630
|
+
expr.ExprIdent("query"),
|
631
|
+
),
|
632
|
+
]
|
633
|
+
)
|
634
|
+
],
|
635
|
+
)
|
636
|
+
)
|
637
|
+
),
|
638
|
+
),
|
639
|
+
)
|
640
|
+
|
641
|
+
outmod.write(program)
|
642
|
+
|
643
|
+
def make_query_processor(cls: Class, pkg: Package):
|
644
|
+
if not cls.is_public:
|
645
|
+
# skip classes that are not public
|
646
|
+
return
|
647
|
+
|
648
|
+
outmod = pkg.module(cls.name + "Query")
|
649
|
+
|
650
|
+
program = Program()
|
651
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
652
|
+
program.import_(f"sera-db.QueryProcessor", True)
|
653
|
+
|
654
|
+
query_args = []
|
655
|
+
for prop in cls.properties.values():
|
656
|
+
propname = to_camel_case(prop.name)
|
657
|
+
if propname != prop.name:
|
658
|
+
query_args.append(
|
659
|
+
(
|
660
|
+
expr.ExprIdent(propname),
|
661
|
+
expr.ExprConstant(prop.name),
|
662
|
+
)
|
663
|
+
)
|
664
|
+
|
665
|
+
program.root(
|
666
|
+
stmt.LineBreak(),
|
667
|
+
stmt.TypescriptStatement(
|
668
|
+
f"export const query = "
|
669
|
+
+ expr.ExprNewInstance(
|
670
|
+
expr.ExprIdent(f"QueryProcessor<{cls.name}>"),
|
671
|
+
[
|
672
|
+
PredefinedFn.dict(query_args),
|
673
|
+
],
|
674
|
+
).to_typescript()
|
675
|
+
+ ";",
|
676
|
+
),
|
677
|
+
)
|
678
|
+
|
679
|
+
outmod.write(program)
|
680
|
+
|
681
|
+
def make_definition(cls: Class, pkg: Package):
|
682
|
+
"""Make schema definition for the class in frontend so that we can generate components"""
|
683
|
+
if not cls.is_public:
|
684
|
+
# skip classes that are not public
|
685
|
+
return
|
686
|
+
|
687
|
+
program = Program()
|
688
|
+
prop_defs: list[tuple[expr.Expr, expr.Expr]] = []
|
689
|
+
|
690
|
+
for prop in cls.properties.values():
|
691
|
+
if prop.data.is_private:
|
692
|
+
# skip private fields as this is for APIs exchange
|
693
|
+
continue
|
694
|
+
propname = to_camel_case(prop.name)
|
695
|
+
tsprop = {}
|
696
|
+
|
697
|
+
if isinstance(prop, DataProperty):
|
698
|
+
tstype = prop.get_data_model_datatype().get_typescript_type()
|
699
|
+
# for schema definition, we need to use the original type, not the type alias
|
700
|
+
# if prop.name == idprop.name:
|
701
|
+
# # use id type alias
|
702
|
+
# tstype = TsTypeWithDep(f"{cls.name}Id")
|
703
|
+
if tstype.dep is not None:
|
704
|
+
program.import_(tstype.dep, True)
|
705
|
+
tsprop = [
|
706
|
+
(expr.ExprIdent("datatype"), expr.ExprConstant(tstype.type)),
|
707
|
+
(
|
708
|
+
expr.ExprIdent("isList"),
|
709
|
+
expr.ExprConstant(prop.datatype.is_list),
|
710
|
+
),
|
711
|
+
]
|
712
|
+
else:
|
713
|
+
assert isinstance(prop, ObjectProperty)
|
714
|
+
if prop.target.db is not None:
|
715
|
+
# this class is stored in the database, we store the id instead
|
716
|
+
tstype = (
|
717
|
+
assert_not_null(prop.target.get_id_property())
|
718
|
+
.get_data_model_datatype()
|
719
|
+
.get_typescript_type()
|
720
|
+
)
|
721
|
+
else:
|
722
|
+
# we are going to store the whole object
|
723
|
+
tstype = TsTypeWithDep(
|
724
|
+
prop.target.name,
|
725
|
+
f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}",
|
726
|
+
)
|
727
|
+
|
728
|
+
# we don't store the type itself, but just the name of the type
|
729
|
+
# so not need to import the dependency
|
730
|
+
# if tstype.dep is not None:
|
731
|
+
# program.import_(
|
732
|
+
# tstype.dep,
|
733
|
+
# True,
|
734
|
+
# )
|
735
|
+
|
736
|
+
tsprop = [
|
737
|
+
(
|
738
|
+
expr.ExprIdent("targetClass"),
|
739
|
+
expr.ExprConstant(prop.target.name),
|
740
|
+
),
|
741
|
+
(expr.ExprIdent("datatype"), expr.ExprConstant(tstype.type)),
|
742
|
+
(
|
743
|
+
expr.ExprIdent("isList"),
|
744
|
+
expr.ExprConstant(prop.cardinality.is_star_to_many()),
|
745
|
+
),
|
746
|
+
(
|
747
|
+
expr.ExprIdent("isEmbedded"),
|
748
|
+
expr.ExprConstant(prop.target.db is not None),
|
749
|
+
),
|
750
|
+
]
|
751
|
+
|
752
|
+
prop_defs.append(
|
753
|
+
(
|
754
|
+
expr.ExprIdent(propname),
|
755
|
+
PredefinedFn.dict(
|
756
|
+
[
|
757
|
+
(expr.ExprIdent("name"), expr.ExprConstant(prop.name)),
|
758
|
+
(
|
759
|
+
expr.ExprIdent("label"),
|
760
|
+
expr.ExprConstant(prop.label.to_dict()),
|
761
|
+
),
|
762
|
+
(
|
763
|
+
expr.ExprIdent("description"),
|
764
|
+
expr.ExprConstant(prop.description.to_dict()),
|
765
|
+
),
|
766
|
+
]
|
767
|
+
+ tsprop
|
768
|
+
),
|
769
|
+
)
|
770
|
+
)
|
771
|
+
|
772
|
+
program.import_("sera-db.Schema", True)
|
773
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
774
|
+
program.root(
|
775
|
+
stmt.LineBreak(),
|
776
|
+
stmt.TypescriptStatement(
|
777
|
+
f"export const {cls.name}Schema: Schema<{cls.name}> = "
|
778
|
+
+ PredefinedFn.dict(
|
779
|
+
[
|
780
|
+
(expr.ExprIdent("properties"), PredefinedFn.dict(prop_defs)),
|
781
|
+
]
|
782
|
+
+ (
|
783
|
+
[
|
784
|
+
(
|
785
|
+
expr.ExprIdent("primaryKey"),
|
786
|
+
expr.ExprConstant(
|
787
|
+
assert_not_null(cls.get_id_property()).name
|
788
|
+
),
|
789
|
+
)
|
790
|
+
]
|
791
|
+
if cls.db is not None
|
792
|
+
else []
|
793
|
+
)
|
794
|
+
).to_typescript()
|
795
|
+
+ ";"
|
796
|
+
),
|
797
|
+
)
|
798
|
+
pkg.module(cls.name + "Schema").write(program)
|
799
|
+
|
800
|
+
def make_index(pkg: Package):
|
801
|
+
outmod = pkg.module("index")
|
802
|
+
if outmod.exists():
|
803
|
+
# skip if the module already exists
|
804
|
+
logger.info(f"Module {outmod.path} already exists, skip")
|
805
|
+
return
|
806
|
+
|
807
|
+
program = Program()
|
808
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
809
|
+
if cls.db is not None:
|
810
|
+
# only import the id if this class is stored in the database
|
811
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id", True)
|
812
|
+
program.import_(
|
813
|
+
f"@.models.{pkg.dir.name}.{cls.name}Schema.{cls.name}Schema", True
|
814
|
+
)
|
815
|
+
program.import_(
|
816
|
+
f"@.models.{pkg.dir.name}.Draft{cls.name}.Draft{cls.name}", True
|
817
|
+
)
|
818
|
+
if cls.db is not None:
|
819
|
+
program.import_(
|
820
|
+
f"@.models.{pkg.dir.name}.{cls.name}Table.{cls.name}Table", True
|
821
|
+
)
|
822
|
+
|
823
|
+
program.root(
|
824
|
+
stmt.LineBreak(),
|
825
|
+
stmt.TypescriptStatement(
|
826
|
+
f"export {{ {cls.name}, Draft{cls.name}, {cls.name}Table, {cls.name}Schema }};"
|
827
|
+
if cls.db is not None
|
828
|
+
else f"export {{ {cls.name}, Draft{cls.name}, {cls.name}Schema }};"
|
829
|
+
),
|
830
|
+
(
|
831
|
+
stmt.TypescriptStatement(f"export type {{ {cls.name}Id }};")
|
832
|
+
if cls.db
|
833
|
+
else None
|
834
|
+
),
|
835
|
+
)
|
836
|
+
|
837
|
+
outmod.write(program)
|
838
|
+
|
839
|
+
for cls in schema.topological_sort():
|
840
|
+
pkg = target_pkg.pkg(cls.get_tsmodule_name())
|
841
|
+
make_normal(cls, pkg)
|
842
|
+
make_draft(cls, pkg)
|
843
|
+
make_query_processor(cls, pkg)
|
844
|
+
make_table(cls, pkg)
|
845
|
+
make_definition(cls, pkg)
|
846
|
+
|
847
|
+
make_index(pkg)
|