sera-2 1.2.1__py3-none-any.whl → 1.4.3__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.
@@ -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)