Compare commits
524 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d44a265ca | ||
|
|
4442937ebd | ||
|
|
59869e077e | ||
|
|
971e09a72a | ||
|
|
bf2b490731 | ||
|
|
fd41a53404 | ||
|
|
a48e907e50 | ||
|
|
a117bad342 | ||
|
|
190cf626f9 | ||
|
|
2bc34acacf | ||
|
|
5f4acbea51 | ||
|
|
6d4ada7b31 | ||
|
|
1bccee3429 | ||
|
|
947d3d0694 | ||
|
|
e76fdbe1ab | ||
|
|
db8be34607 | ||
|
|
407c3b359b | ||
|
|
a2652c983f | ||
|
|
42fe568e2b | ||
|
|
ea7f8d1acc | ||
|
|
af4e5de1ad | ||
|
|
cc80081280 | ||
|
|
444a1b9dcc | ||
|
|
91637ba7a3 | ||
|
|
be7db6ad96 | ||
|
|
a3b550859c | ||
|
|
c58f8c0bf8 | ||
|
|
73304e8af4 | ||
|
|
e21c61b5e3 | ||
|
|
78a44b5189 | ||
|
|
da1d912bdb | ||
|
|
c45a843611 | ||
|
|
b07099b57d | ||
|
|
a7688e4ed5 | ||
|
|
9c91d99bed | ||
|
|
a1ea192a73 | ||
|
|
52fb1ec183 | ||
|
|
b51d396afc | ||
|
|
c8d8254fc1 | ||
|
|
83455f1e83 | ||
|
|
c779be2897 | ||
|
|
8c40144734 | ||
|
|
022cd47e7e | ||
|
|
c7a05c3213 | ||
|
|
a60c6d173c | ||
|
|
971da3e57c | ||
|
|
2f29f43409 | ||
|
|
56c38c04aa | ||
|
|
6b96ee9856 | ||
|
|
23b0c45518 | ||
|
|
10649d9fbf | ||
|
|
6a6cd7b910 | ||
|
|
2958d38c63 | ||
|
|
aecd9a8245 | ||
|
|
4f3439e835 | ||
|
|
a5bec5baf7 | ||
|
|
8d23453371 | ||
|
|
2184c4a7e1 | ||
|
|
ba5e36fa55 | ||
|
|
70d1d48fbc | ||
|
|
d23026121e | ||
|
|
057b038fac | ||
|
|
0697f3d363 | ||
|
|
400d44289c | ||
|
|
bbc3354f16 | ||
|
|
d038d72cde | ||
|
|
16e54d20d0 | ||
|
|
14775eb556 | ||
|
|
ce34bae16a | ||
|
|
640cdcf671 | ||
|
|
f15924c0be | ||
|
|
0d32c5b4b3 | ||
|
|
101050ce58 | ||
|
|
b16249e7c2 | ||
|
|
8b63344bc2 | ||
|
|
b648175205 | ||
|
|
4bf1bc35ae | ||
|
|
067273d428 | ||
|
|
7ed284d74b | ||
|
|
f65d3385ec | ||
|
|
7635355e94 | ||
|
|
ec75c7ecdb | ||
|
|
786420d1d2 | ||
|
|
cff0ce1e1a | ||
|
|
8355f985cd | ||
|
|
25af538c88 | ||
|
|
d1503cd813 | ||
|
|
7d2a33b3ec | ||
|
|
752686e3e1 | ||
|
|
3870f34ef8 | ||
|
|
ae71a066c7 | ||
|
|
b52327db2e | ||
|
|
d5c089e07e | ||
|
|
0f78c624b1 | ||
|
|
e1632c41c2 | ||
|
|
323b051355 | ||
|
|
3999b17e88 | ||
|
|
8fd15614af | ||
|
|
ddefc2737b | ||
|
|
05868d8a09 | ||
|
|
b3afb988a3 | ||
|
|
3b8e0573f2 | ||
|
|
4779a6e4af | ||
|
|
39ac149430 | ||
|
|
8ec7db031b | ||
|
|
25b5b91a19 | ||
|
|
e8c6f6ffb9 | ||
|
|
62d8cd7b05 | ||
|
|
c09159d6ce | ||
|
|
8d2db95540 | ||
|
|
9519846489 | ||
|
|
f7a977df33 | ||
|
|
f1717920b6 | ||
|
|
c5ab17ad99 | ||
|
|
1839c3c57b | ||
|
|
ba63b3504f | ||
|
|
032e336b65 | ||
|
|
474e46837a | ||
|
|
e7052636ba | ||
|
|
cb2631061e | ||
|
|
d1ddfe7d31 | ||
|
|
59cad22183 | ||
|
|
57d64a7ef8 | ||
|
|
ae096eb4c3 | ||
|
|
789861999c | ||
|
|
72f4bd066d | ||
|
|
b3db65d1b8 | ||
|
|
506a9cd9c0 | ||
|
|
1ead06fd4f | ||
|
|
eecd947377 | ||
|
|
5351e3ea57 | ||
|
|
3bdb77888f | ||
|
|
c570fd6ae3 | ||
|
|
be3ed4af5d | ||
|
|
4cce044128 | ||
|
|
59875cf900 | ||
|
|
37129055e6 | ||
|
|
934e80c2ab | ||
|
|
8e20fbd24d | ||
|
|
f102069f5a | ||
|
|
afc36161ed | ||
|
|
a8b76bc21a | ||
|
|
8550bd31d9 | ||
|
|
8837494a06 | ||
|
|
0c407b81b7 | ||
|
|
71b4a02592 | ||
|
|
83e5767812 | ||
|
|
c544c2c7f9 | ||
|
|
818c8fbdf9 | ||
|
|
a6326f149d | ||
|
|
01679697b4 | ||
|
|
d4fb2a8ccc | ||
|
|
08b6437a1e | ||
|
|
baffd9d05c | ||
|
|
cbff7c130c | ||
|
|
16f3d1a320 | ||
|
|
955ea1a9ed | ||
|
|
ca614f6cc2 | ||
|
|
71748f6aa0 | ||
|
|
80b639b511 | ||
|
|
bba68da488 | ||
|
|
29c2b53f53 | ||
|
|
c3cc248a39 | ||
|
|
fb821dbf21 | ||
|
|
079250fcd7 | ||
|
|
120cb5fadd | ||
|
|
d3a554108f | ||
|
|
6471158847 | ||
|
|
1c442eb195 | ||
|
|
13f5660fee | ||
|
|
9333a8318c | ||
|
|
c1cda5fa62 | ||
|
|
88967ba9d3 | ||
|
|
92d792246c | ||
|
|
586aaec506 | ||
|
|
10690b5a6e | ||
|
|
bceef9777a | ||
|
|
4f786cdcc3 | ||
|
|
8e226615eb | ||
|
|
82734e8383 | ||
|
|
69a83c584b | ||
|
|
a8fdcd179e | ||
|
|
ace976965d | ||
|
|
7303d1ea0b | ||
|
|
4379b0b955 | ||
|
|
09af7af228 | ||
|
|
dc08da211f | ||
|
|
30e1df0dd8 | ||
|
|
95a4c977c1 | ||
|
|
6ce081196c | ||
|
|
3d5342b314 | ||
|
|
78d43e6859 | ||
|
|
41106ae306 | ||
|
|
33aa2ddd45 | ||
|
|
2be5505c55 | ||
|
|
8c0f07cc51 | ||
|
|
3018b1f2e1 | ||
|
|
a21a2314d7 | ||
|
|
a76aae3d12 | ||
|
|
7765067d1b | ||
|
|
eddbe5fa3f | ||
|
|
c907d2773d | ||
|
|
5f71e56bf9 | ||
|
|
adcbd1a95a | ||
|
|
175a61c81c | ||
|
|
4d97f24531 | ||
|
|
8d32d704b5 | ||
|
|
e5d4a5f95f | ||
|
|
d4a0f78cd0 | ||
|
|
7cd946181e | ||
|
|
cf97a3ba5e | ||
|
|
963e0c906c | ||
|
|
089743ac23 | ||
|
|
69ef120677 | ||
|
|
fe2e6a53e9 | ||
|
|
cf1b5e7f71 | ||
|
|
202002358a | ||
|
|
14eb28d37f | ||
|
|
81dbbdd6f5 | ||
|
|
9e6787fb3f | ||
|
|
2eee7bb0c1 | ||
|
|
7f57ecc35e | ||
|
|
21f6130666 | ||
|
|
594b3dac4a | ||
|
|
ef2b279df6 | ||
|
|
2ffd7a6151 | ||
|
|
045d32c245 | ||
|
|
053588ae74 | ||
|
|
749a2d6f59 | ||
|
|
95ba8f0b33 | ||
|
|
dacf6cb7f8 | ||
|
|
656c3b3d09 | ||
|
|
44ce6636c0 | ||
|
|
1413630f11 | ||
|
|
8f55f63f77 | ||
|
|
0331ffeb93 | ||
|
|
196b74bebb | ||
|
|
305e137a1a | ||
|
|
4e5ddc8027 | ||
|
|
4bb75de3f0 | ||
|
|
0572a0eb50 | ||
|
|
c13cb40c7b | ||
|
|
33787ba796 | ||
|
|
64f4468664 | ||
|
|
408b65be30 | ||
|
|
891420cb09 | ||
|
|
a657c59b2c | ||
|
|
89ec084106 | ||
|
|
a7a0daaf82 | ||
|
|
df5c2a3141 | ||
|
|
f902f5298c | ||
|
|
ddd038761b | ||
|
|
09e53244d9 | ||
|
|
714e144329 | ||
|
|
e1b3dfb00a | ||
|
|
b6a4607e60 | ||
|
|
9553cc811a | ||
|
|
59c05b3628 | ||
|
|
d3629a8a09 | ||
|
|
a17e8537fb | ||
|
|
a7f23c5885 | ||
|
|
b706191a0e | ||
|
|
ba469ef900 | ||
|
|
e852346b94 | ||
|
|
02d24eccd8 | ||
|
|
d1359ccc36 | ||
|
|
52c7f1c7ba | ||
|
|
7a2749c405 | ||
|
|
d71df901ed | ||
|
|
1af4b6c2e4 | ||
|
|
2595cb8565 | ||
|
|
45d549aa4e | ||
|
|
7f65f5e40e | ||
|
|
5ce1cc4e6a | ||
|
|
3a6d60e9a8 | ||
|
|
d5a09f359d | ||
|
|
127e95ca1c | ||
|
|
bb81126cd8 | ||
|
|
2d3d120f81 | ||
|
|
0c36c4a4e5 | ||
|
|
88f6686809 | ||
|
|
9c7b682a36 | ||
|
|
dafdbf0a84 | ||
|
|
5ac8e9b484 | ||
|
|
753c5929e1 | ||
|
|
e3f46d775a | ||
|
|
0eb3a78332 | ||
|
|
3ac9f25284 | ||
|
|
b3c9c8f37c | ||
|
|
32bc126def | ||
|
|
00a5f47cae | ||
|
|
6a1260687b | ||
|
|
7591787583 | ||
|
|
bd961a03d4 | ||
|
|
8fe816dddc | ||
|
|
e7a8dc86eb | ||
|
|
c9dc891481 | ||
|
|
89c3873db7 | ||
|
|
60352d7932 | ||
|
|
664f2af346 | ||
|
|
8212e906a3 | ||
|
|
92e17a9f43 | ||
|
|
d3727ad2f7 | ||
|
|
391e5d9992 | ||
|
|
a4bd585730 | ||
|
|
c694769f4c | ||
|
|
8b9ff9793c | ||
|
|
8ba4566d23 | ||
|
|
91420b9973 | ||
|
|
8d3e0423e7 | ||
|
|
4bafc3a61c | ||
|
|
1f43df6d41 | ||
|
|
c2a54e29f8 | ||
|
|
b1f9073f4d | ||
|
|
1b38e2412c | ||
|
|
4b9311713a | ||
|
|
77520ee46a | ||
|
|
23c07a3570 | ||
|
|
1451225978 | ||
|
|
51fd9fcd13 | ||
|
|
1fe77c0905 | ||
|
|
cd739fb52e | ||
|
|
9e845843d8 | ||
|
|
0cc280ed55 | ||
|
|
b3707d21b2 | ||
|
|
fbebd6c1c1 | ||
|
|
d7c2bda461 | ||
|
|
2bf949513b | ||
|
|
84619fb656 | ||
|
|
b600f16ecd | ||
|
|
9273066f61 | ||
|
|
7d59dbcf84 | ||
|
|
015d1ae95b | ||
|
|
e2cd6e0e5e | ||
|
|
ec113058d0 | ||
|
|
d2ac2bfdd8 | ||
|
|
d75fe18e6a | ||
|
|
479f222b54 | ||
|
|
013c536b47 | ||
|
|
3b983a0db5 | ||
|
|
5f9559ac8d | ||
|
|
f487e6d765 | ||
|
|
5e26422e9c | ||
|
|
64baebfaaa | ||
|
|
521dec24b2 | ||
|
|
36f0bd8eb9 | ||
|
|
d0a2b122b2 | ||
|
|
c80cc8ec86 | ||
|
|
3722bcf8c8 | ||
|
|
0372d213c0 | ||
|
|
c322eb1e5a | ||
|
|
b34dcac685 | ||
|
|
4850f50c66 | ||
|
|
5996f819e8 | ||
|
|
4d967fe7a2 | ||
|
|
bb91c2bbe5 | ||
|
|
511df52c3c | ||
|
|
d42e1da14b | ||
|
|
75dbd78da1 | ||
|
|
c90b7785c0 | ||
|
|
c17af04cbf | ||
|
|
f5e3a9a4a2 | ||
|
|
dab3391aa2 | ||
|
|
0336c55560 | ||
|
|
8e618ab443 | ||
|
|
352d672bdd | ||
|
|
df64c0a4b5 | ||
|
|
83597d9e02 | ||
|
|
a09220b881 | ||
|
|
5623f3af09 | ||
|
|
820b5e8570 | ||
|
|
dc72ed2feb | ||
|
|
ea468c9878 | ||
|
|
d1b683344e | ||
|
|
a82ec7de3f | ||
|
|
560a9efc69 | ||
|
|
4f8b1e33fa | ||
|
|
38dd51f757 | ||
|
|
38f23cc6ae | ||
|
|
6cf8fa8a9c | ||
|
|
f9ea4715d7 | ||
|
|
b34b374f76 | ||
|
|
83d1168f26 | ||
|
|
91009f52cd | ||
|
|
c6dfca7052 | ||
|
|
aaeaeeed24 | ||
|
|
c5804f764c | ||
|
|
fbe0d1bcd1 | ||
|
|
2fb440f033 | ||
|
|
a8a136a9ce | ||
|
|
fcbb903839 | ||
|
|
ac45a2ba26 | ||
|
|
afe15dd4f5 | ||
|
|
e3df88bea0 | ||
|
|
c69a414f78 | ||
|
|
d08022ab94 | ||
|
|
66e6fab663 | ||
| 4da572822e | |||
| ee23bb3ba3 | |||
| d002e340dd | |||
| 0e1d87ddab | |||
| 2a4928c1b6 | |||
| efe2bd57ab | |||
|
|
a0aa678e7d | ||
|
|
a1b6e6ab59 | ||
|
|
73acf1d1cd | ||
|
|
48110e9a6f | ||
|
|
642e215c69 | ||
|
|
091b9ff70a | ||
|
|
86f753c745 | ||
|
|
c28f8b1384 | ||
|
|
9b36297171 | ||
|
|
7beed235d7 | ||
|
|
a0206dc8cb | ||
|
|
bf0eed3b03 | ||
|
|
c8072b8052 | ||
|
|
c66fbf1a62 | ||
|
|
e13a711a60 | ||
|
|
346a326bfd | ||
|
|
addb8e9a6d | ||
|
|
ea8b9e661d | ||
|
|
339ae844e9 | ||
|
|
a0a7e81927 | ||
|
|
31c23a0c40 | ||
|
|
c1f22246ea | ||
|
|
0a1388bf06 | ||
|
|
1a69b83983 | ||
|
|
63f9443b77 | ||
|
|
6a9b2b8d1d | ||
|
|
8e1e0968ae | ||
|
|
a486292880 | ||
|
|
ee4b0ee7c2 | ||
|
|
43d86cce18 | ||
|
|
25d7c70058 | ||
|
|
71c62cf5e8 | ||
|
|
a7350282ee | ||
|
|
676629bd8d | ||
|
|
1892877b11 | ||
|
|
be218aabf7 | ||
|
|
856f7d56bf | ||
|
|
000ebbdc2b | ||
|
|
791314bef2 | ||
|
|
bcb0b01324 | ||
|
|
03e3a21a25 | ||
|
|
e97a2a62c9 | ||
|
|
814f972287 | ||
|
|
274c2a3292 | ||
|
|
4dbcebfab8 | ||
|
|
fadc301d41 | ||
|
|
b1d29f2083 | ||
|
|
e756b3692d | ||
|
|
74a3d59800 | ||
|
|
0544a3dfde | ||
|
|
656c821986 | ||
|
|
865ef81012 | ||
|
|
5ad27a87e5 | ||
|
|
085b851925 | ||
|
|
98dea7dd39 | ||
|
|
e5ef334f7c | ||
|
|
d6ea09b3e2 | ||
|
|
a51b8a1ff6 | ||
|
|
3c885b6ab9 | ||
|
|
6b3b30108b | ||
|
|
7fab23d22b | ||
|
|
def88f6486 | ||
|
|
1797ae3e58 | ||
|
|
f768ba3b27 | ||
|
|
b3e48a0b06 | ||
|
|
3f56939421 | ||
|
|
87c720c3fe | ||
|
|
90fbcaf31d | ||
|
|
56c3569b68 | ||
|
|
e2969c1837 | ||
|
|
fe14c7b9f5 | ||
|
|
5d01b24c2d | ||
|
|
4eeb5021ee | ||
|
|
6ec62af606 | ||
|
|
3d6fdc65d2 | ||
|
|
956418f5f3 | ||
|
|
e57de7f983 | ||
|
|
08e2c87de8 | ||
|
|
ba1a12402d | ||
|
|
39716b1f40 | ||
|
|
adc7132404 | ||
|
|
8c8841705c | ||
|
|
f7fdd8ab08 | ||
|
|
5807c6f3d3 | ||
|
|
7e0691eea3 | ||
|
|
17d4d21620 | ||
|
|
d19feb8bc1 | ||
|
|
ab1e4bec60 | ||
|
|
672cec9c2a | ||
|
|
c3ea7eecc2 | ||
|
|
608e62c2bd | ||
|
|
c1b69389c6 | ||
|
|
182f38597c | ||
|
|
06ea259dc9 | ||
|
|
29dd7ec80c | ||
|
|
3f043fc315 | ||
|
|
5ed27e5a6a | ||
|
|
23725c20ee | ||
|
|
29b6db7ee9 | ||
|
|
6e7165fe7f | ||
|
|
43131250ed | ||
|
|
c3beb029e5 | ||
|
|
9f10ac9e96 | ||
|
|
d36901aa2b | ||
|
|
4510aa3d14 | ||
|
|
3b8736acd7 | ||
|
|
735075d1bd | ||
|
|
dc7001a80c | ||
|
|
8a9acf6c4a | ||
|
|
5ca017950e | ||
|
|
eadec50e30 | ||
|
|
e7f5918013 | ||
|
|
27b675cb19 | ||
|
|
016a37c116 | ||
|
|
d8b1efc3ca | ||
|
|
d13fe19198 | ||
|
|
762a2e9cf0 | ||
|
|
44a2c525e7 | ||
|
|
507b0275d3 | ||
|
|
ccd8bfba0d | ||
|
|
47f5def67c |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
.depbe.sh
|
.depbe.sh
|
||||||
node_modules
|
node_modules
|
||||||
node_modules/*
|
node_modules/*
|
||||||
|
**/package-lock.json
|
||||||
backend/.env
|
backend/.env
|
||||||
backend/images
|
backend/images
|
||||||
backend/images/*
|
backend/images/*
|
||||||
@@ -17,3 +18,9 @@ frontend/dist
|
|||||||
frontend/dist/*
|
frontend/dist/*
|
||||||
frontedtree.txt
|
frontedtree.txt
|
||||||
backend/dist/
|
backend/dist/
|
||||||
|
backend/data/model-cache
|
||||||
|
build
|
||||||
|
build/*
|
||||||
|
.vscode
|
||||||
|
.vscode/*
|
||||||
|
.clang-format
|
||||||
|
|||||||
156
CHURCH_MODELS.md
Normal file
156
CHURCH_MODELS.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Church Models - Übersicht für Daemon-Entwicklung
|
||||||
|
|
||||||
|
## 1. ChurchOfficeType (falukant_type.church_office_type)
|
||||||
|
|
||||||
|
**Schema:** `falukant_type`
|
||||||
|
**Tabelle:** `church_office_type`
|
||||||
|
**Zweck:** Definiert die verschiedenen Kirchenämter-Typen
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: INTEGER (PK, auto-increment)
|
||||||
|
name: STRING (z.B. "pope", "cardinal", "lay-preacher")
|
||||||
|
seatsPerRegion: INTEGER (Anzahl verfügbarer Plätze pro Region)
|
||||||
|
regionType: STRING (z.B. "country", "duchy", "city")
|
||||||
|
hierarchyLevel: INTEGER (0-8, höhere Zahl = höhere Position)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beziehungen:**
|
||||||
|
- `hasMany` ChurchOffice (als `offices`)
|
||||||
|
- `hasMany` ChurchApplication (als `applications`)
|
||||||
|
- `hasMany` ChurchOfficeRequirement (als `requirements`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ChurchOfficeRequirement (falukant_predefine.church_office_requirement)
|
||||||
|
|
||||||
|
**Schema:** `falukant_predefine`
|
||||||
|
**Tabelle:** `church_office_requirement`
|
||||||
|
**Zweck:** Definiert Voraussetzungen für Kirchenämter
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: INTEGER (PK, auto-increment)
|
||||||
|
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||||
|
prerequisiteOfficeTypeId: INTEGER (FK -> ChurchOfficeType.id, nullable)
|
||||||
|
minTitleLevel: INTEGER (nullable, optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beziehungen:**
|
||||||
|
- `belongsTo` ChurchOfficeType (als `officeType`)
|
||||||
|
- `belongsTo` ChurchOfficeType (als `prerequisiteOfficeType`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ChurchOffice (falukant_data.church_office)
|
||||||
|
|
||||||
|
**Schema:** `falukant_data`
|
||||||
|
**Tabelle:** `church_office`
|
||||||
|
**Zweck:** Speichert tatsächlich besetzte Kirchenämter
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: INTEGER (PK, auto-increment)
|
||||||
|
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||||
|
characterId: INTEGER (FK -> FalukantCharacter.id)
|
||||||
|
regionId: INTEGER (FK -> RegionData.id)
|
||||||
|
supervisorId: INTEGER (FK -> FalukantCharacter.id, nullable)
|
||||||
|
createdAt: DATE
|
||||||
|
updatedAt: DATE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beziehungen:**
|
||||||
|
- `belongsTo` ChurchOfficeType (als `type`)
|
||||||
|
- `belongsTo` FalukantCharacter (als `holder`)
|
||||||
|
- `belongsTo` FalukantCharacter (als `supervisor`)
|
||||||
|
- `belongsTo` RegionData (als `region`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ChurchApplication (falukant_data.church_application)
|
||||||
|
|
||||||
|
**Schema:** `falukant_data`
|
||||||
|
**Tabelle:** `church_application`
|
||||||
|
**Zweck:** Speichert Bewerbungen für Kirchenämter
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: INTEGER (PK, auto-increment)
|
||||||
|
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||||
|
characterId: INTEGER (FK -> FalukantCharacter.id)
|
||||||
|
regionId: INTEGER (FK -> RegionData.id)
|
||||||
|
supervisorId: INTEGER (FK -> FalukantCharacter.id)
|
||||||
|
status: ENUM('pending', 'approved', 'rejected')
|
||||||
|
decisionDate: DATE (nullable)
|
||||||
|
createdAt: DATE
|
||||||
|
updatedAt: DATE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beziehungen:**
|
||||||
|
- `belongsTo` ChurchOfficeType (als `officeType`)
|
||||||
|
- `belongsTo` FalukantCharacter (als `applicant`)
|
||||||
|
- `belongsTo` FalukantCharacter (als `supervisor`)
|
||||||
|
- `belongsTo` RegionData (als `region`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zusätzlich benötigte Models (für Daemon)
|
||||||
|
|
||||||
|
### RegionData (falukant_data.region)
|
||||||
|
- Wird für `regionId` in ChurchOffice und ChurchApplication benötigt
|
||||||
|
- Enthält `regionType` (country, duchy, markgravate, shire, county, city)
|
||||||
|
- Enthält `parentId` für Hierarchie
|
||||||
|
|
||||||
|
### FalukantCharacter (falukant_data.character)
|
||||||
|
- Wird für `characterId` (Inhaber/Bewerber) benötigt
|
||||||
|
- Wird für `supervisorId` benötigt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wichtige Queries für Daemon
|
||||||
|
|
||||||
|
### Verfügbare Positionen finden
|
||||||
|
```sql
|
||||||
|
SELECT cot.*, COUNT(co.id) as occupied_seats
|
||||||
|
FROM falukant_type.church_office_type cot
|
||||||
|
LEFT JOIN falukant_data.church_office co
|
||||||
|
ON cot.id = co.office_type_id
|
||||||
|
AND co.region_id = ?
|
||||||
|
WHERE cot.region_type = ?
|
||||||
|
GROUP BY cot.id
|
||||||
|
HAVING COUNT(co.id) < cot.seats_per_region
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supervisor finden
|
||||||
|
```sql
|
||||||
|
SELECT co.*
|
||||||
|
FROM falukant_data.church_office co
|
||||||
|
JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id
|
||||||
|
WHERE co.region_id = ?
|
||||||
|
AND cot.hierarchy_level > (
|
||||||
|
SELECT hierarchy_level
|
||||||
|
FROM falukant_type.church_office_type
|
||||||
|
WHERE id = ?
|
||||||
|
)
|
||||||
|
ORDER BY cot.hierarchy_level ASC
|
||||||
|
LIMIT 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voraussetzungen prüfen
|
||||||
|
```sql
|
||||||
|
SELECT cor.*
|
||||||
|
FROM falukant_predefine.church_office_requirement cor
|
||||||
|
WHERE cor.office_type_id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bewerbungen für Supervisor
|
||||||
|
```sql
|
||||||
|
SELECT ca.*
|
||||||
|
FROM falukant_data.church_application ca
|
||||||
|
WHERE ca.supervisor_id = ?
|
||||||
|
AND ca.status = 'pending'
|
||||||
|
```
|
||||||
78
CHURCH_OFFICES.md
Normal file
78
CHURCH_OFFICES.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Kirchenämter - Hierarchie und Verfügbarkeit
|
||||||
|
|
||||||
|
## Regionstypen
|
||||||
|
- **country** (Land): Falukant
|
||||||
|
- **duchy** (Herzogtum): Hessen
|
||||||
|
- **markgravate** (Markgrafschaft): Groß-Benbach
|
||||||
|
- **shire** (Grafschaft): Siebenbachen
|
||||||
|
- **county** (Kreis): Bad Homburg, Maintal
|
||||||
|
- **city** (Stadt): Frankfurt, Oberursel, Offenbach, Königstein
|
||||||
|
|
||||||
|
## Kirchenämter (von höchstem zu niedrigstem Rang)
|
||||||
|
|
||||||
|
| Amt | Translation Key | Hierarchie-Level | Regionstyp | Plätze pro Region | Beschreibung |
|
||||||
|
|-----|----------------|-------------------|------------|-------------------|--------------|
|
||||||
|
| **Papst** | `pope` | 8 | country | 1 | Höchstes Amt, nur einer im ganzen Land |
|
||||||
|
| **Kardinal** | `cardinal` | 7 | country | 3 | Höchste Kardinäle, mehrere pro Land möglich |
|
||||||
|
| **Erzbischof** | `archbishop` | 6 | duchy | 1 | Pro Herzogtum ein Erzbischof |
|
||||||
|
| **Bischof** | `bishop` | 5 | markgravate | 1 | Pro Markgrafschaft ein Bischof |
|
||||||
|
| **Erzdiakon** | `archdeacon` | 4 | shire | 1 | Pro Grafschaft ein Erzdiakon |
|
||||||
|
| **Dekan** | `dean` | 3 | county | 1 | Pro Kreis ein Dekan |
|
||||||
|
| **Pfarrer** | `parish-priest` | 2 | city | 1 | Pro Stadt ein Pfarrer |
|
||||||
|
| **Dorfgeistlicher** | `village-priest` | 1 | city | 1 | Pro Stadt ein Dorfgeistlicher (Einstiegsposition) |
|
||||||
|
| **Laienprediger** | `lay-preacher` | 0 | city | 3 | Pro Stadt mehrere Laienprediger (niedrigste Position) |
|
||||||
|
|
||||||
|
## Verfügbare Positionen pro Regionstyp
|
||||||
|
|
||||||
|
### country (Land: Falukant)
|
||||||
|
- **Papst**: 1 Platz
|
||||||
|
- **Kardinal**: 3 Plätze
|
||||||
|
- **Gesamt**: 4 Plätze
|
||||||
|
|
||||||
|
### duchy (Herzogtum: Hessen)
|
||||||
|
- **Erzbischof**: 1 Platz
|
||||||
|
- **Gesamt**: 1 Platz
|
||||||
|
|
||||||
|
### markgravate (Markgrafschaft: Groß-Benbach)
|
||||||
|
- **Bischof**: 1 Platz
|
||||||
|
- **Gesamt**: 1 Platz
|
||||||
|
|
||||||
|
### shire (Grafschaft: Siebenbachen)
|
||||||
|
- **Erzdiakon**: 1 Platz
|
||||||
|
- **Gesamt**: 1 Platz
|
||||||
|
|
||||||
|
### county (Kreis: Bad Homburg, Maintal)
|
||||||
|
- **Dekan**: 1 Platz pro Kreis
|
||||||
|
- **Gesamt**: 1 Platz pro Kreis
|
||||||
|
|
||||||
|
### city (Stadt: Frankfurt, Oberursel, Offenbach, Königstein)
|
||||||
|
- **Pfarrer**: 1 Platz pro Stadt
|
||||||
|
- **Dorfgeistlicher**: 1 Platz pro Stadt
|
||||||
|
- **Laienprediger**: 3 Plätze pro Stadt
|
||||||
|
- **Gesamt**: 5 Plätze pro Stadt
|
||||||
|
|
||||||
|
## Hierarchie und Beförderungsweg
|
||||||
|
|
||||||
|
1. **Laienprediger** (lay-preacher) - Einstiegsposition, keine Voraussetzung
|
||||||
|
2. **Dorfgeistlicher** (village-priest) - Voraussetzung: Laienprediger
|
||||||
|
3. **Pfarrer** (parish-priest) - Voraussetzung: Dorfgeistlicher
|
||||||
|
4. **Dekan** (dean) - Voraussetzung: Pfarrer
|
||||||
|
5. **Erzdiakon** (archdeacon) - Voraussetzung: Dekan
|
||||||
|
6. **Bischof** (bishop) - Voraussetzung: Erzdiakon
|
||||||
|
7. **Erzbischof** (archbishop) - Voraussetzung: Bischof
|
||||||
|
8. **Kardinal** (cardinal) - Voraussetzung: Erzbischof
|
||||||
|
9. **Papst** (pope) - Voraussetzung: Kardinal
|
||||||
|
|
||||||
|
## Gesamtübersicht verfügbarer Positionen
|
||||||
|
|
||||||
|
- **Papst**: 1 Position (Land)
|
||||||
|
- **Kardinal**: 3 Positionen (Land)
|
||||||
|
- **Erzbischof**: 1 Position (Herzogtum)
|
||||||
|
- **Bischof**: 1 Position (Markgrafschaft)
|
||||||
|
- **Erzdiakon**: 1 Position (Grafschaft)
|
||||||
|
- **Dekan**: 2 Positionen (2 Kreise)
|
||||||
|
- **Pfarrer**: 4 Positionen (4 Städte)
|
||||||
|
- **Dorfgeistlicher**: 4 Positionen (4 Städte)
|
||||||
|
- **Laienprediger**: 12 Positionen (4 Städte × 3)
|
||||||
|
|
||||||
|
**Gesamt**: 30 Positionen im System
|
||||||
119
CMakeLists.txt
Normal file
119
CMakeLists.txt
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(YourPartDaemon VERSION 1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
# C++ Standard and Compiler Settings
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
# Use best available GCC for C++23 support (OpenSUSE Tumbleweed)
|
||||||
|
# Try GCC 15 first (best C++23 support), then GCC 13, then system default
|
||||||
|
find_program(GCC15_CC gcc-15)
|
||||||
|
find_program(GCC15_CXX g++-15)
|
||||||
|
find_program(GCC13_CC gcc-13)
|
||||||
|
find_program(GCC13_CXX g++-13)
|
||||||
|
|
||||||
|
if(GCC15_CC AND GCC15_CXX)
|
||||||
|
set(CMAKE_C_COMPILER ${GCC15_CC})
|
||||||
|
set(CMAKE_CXX_COMPILER ${GCC15_CXX})
|
||||||
|
message(STATUS "Using GCC 15 for best C++23 support")
|
||||||
|
elseif(GCC13_CC AND GCC13_CXX)
|
||||||
|
set(CMAKE_C_COMPILER ${GCC13_CC})
|
||||||
|
set(CMAKE_CXX_COMPILER ${GCC13_CXX})
|
||||||
|
message(STATUS "Using GCC 13 for C++23 support")
|
||||||
|
else()
|
||||||
|
message(STATUS "Using system default compiler")
|
||||||
|
endif()
|
||||||
|
# Optimize for GCC 13 with C++23
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto=auto -O3 -march=native -mtune=native")
|
||||||
|
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -DDEBUG")
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
|
||||||
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
|
|
||||||
|
# Include /usr/local if needed
|
||||||
|
list(APPEND CMAKE_PREFIX_PATH /usr/local)
|
||||||
|
|
||||||
|
# Find libwebsockets via pkg-config
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(LWS REQUIRED libwebsockets)
|
||||||
|
|
||||||
|
# Find other dependencies
|
||||||
|
find_package(PostgreSQL REQUIRED)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
find_package(nlohmann_json CONFIG REQUIRED)
|
||||||
|
|
||||||
|
# PostgreSQL C++ libpqxx
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(LIBPQXX REQUIRED libpqxx)
|
||||||
|
|
||||||
|
# Project sources and headers
|
||||||
|
set(SOURCES
|
||||||
|
src/main.cpp
|
||||||
|
src/config.cpp
|
||||||
|
src/connection_pool.cpp
|
||||||
|
src/database.cpp
|
||||||
|
src/character_creation_worker.cpp
|
||||||
|
src/produce_worker.cpp
|
||||||
|
src/message_broker.cpp
|
||||||
|
src/websocket_server.cpp
|
||||||
|
src/stockagemanager.cpp
|
||||||
|
src/director_worker.cpp
|
||||||
|
src/valuerecalculationworker.cpp
|
||||||
|
src/usercharacterworker.cpp
|
||||||
|
src/houseworker.cpp
|
||||||
|
src/politics_worker.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(HEADERS
|
||||||
|
src/config.h
|
||||||
|
src/database.h
|
||||||
|
src/connection_pool.h
|
||||||
|
src/worker.h
|
||||||
|
src/character_creation_worker.h
|
||||||
|
src/produce_worker.h
|
||||||
|
src/message_broker.h
|
||||||
|
src/websocket_server.h
|
||||||
|
src/stockagemanager.h
|
||||||
|
src/director_worker.h
|
||||||
|
src/valuerecalculationworker.h
|
||||||
|
src/usercharacterworker.h
|
||||||
|
src/houseworker.h
|
||||||
|
src/politics_worker.h
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define executable target
|
||||||
|
add_executable(yourpart-daemon ${SOURCES} ${HEADERS}
|
||||||
|
src/utils.h src/utils.cpp
|
||||||
|
src/underground_worker.h src/underground_worker.cpp)
|
||||||
|
|
||||||
|
# Include directories
|
||||||
|
target_include_directories(yourpart-daemon PRIVATE
|
||||||
|
${PostgreSQL_INCLUDE_DIRS}
|
||||||
|
${LIBPQXX_INCLUDE_DIRS}
|
||||||
|
${LWS_INCLUDE_DIRS}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find systemd
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(SYSTEMD REQUIRED libsystemd)
|
||||||
|
|
||||||
|
# Link libraries
|
||||||
|
target_link_libraries(yourpart-daemon PRIVATE
|
||||||
|
${PostgreSQL_LIBRARIES}
|
||||||
|
Threads::Threads
|
||||||
|
z ssl crypto
|
||||||
|
${LIBPQXX_LIBRARIES}
|
||||||
|
${LWS_LIBRARIES}
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SYSTEMD_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Installation rules
|
||||||
|
install(TARGETS yourpart-daemon DESTINATION /usr/local/bin)
|
||||||
|
|
||||||
|
# Installiere Template als Referenz ZUERST (wird vom install-Skript benötigt)
|
||||||
|
install(FILES daemon.conf DESTINATION /etc/yourpart/ RENAME daemon.conf.example)
|
||||||
|
|
||||||
|
# Intelligente Konfigurationsdatei-Installation
|
||||||
|
# Verwendet ein CMake-Skript, das nur fehlende Keys hinzufügt, ohne bestehende zu überschreiben
|
||||||
|
# Das Skript liest das Template aus /etc/yourpart/daemon.conf.example oder dem Source-Verzeichnis
|
||||||
|
install(SCRIPT cmake/install-config.cmake)
|
||||||
414
CMakeLists.txt.user
Normal file
414
CMakeLists.txt.user
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE QtCreatorProject>
|
||||||
|
<!-- Written by QtCreator 17.0.0, 2025-08-16T22:07:06. -->
|
||||||
|
<qtcreator>
|
||||||
|
<data>
|
||||||
|
<variable>EnvironmentId</variable>
|
||||||
|
<value type="QByteArray">{551ef6b3-a39b-43e2-9ee3-ad56e19ff4f4}</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
||||||
|
<value type="qlonglong">0</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<value type="bool" key="EditorConfiguration.AutoDetect">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
||||||
|
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
||||||
|
<value type="QString" key="language">Cpp</value>
|
||||||
|
<valuemap type="QVariantMap" key="value">
|
||||||
|
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
||||||
|
<value type="QString" key="language">QmlJS</value>
|
||||||
|
<valuemap type="QVariantMap" key="value">
|
||||||
|
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
||||||
|
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.LineEndingBehavior">0</value>
|
||||||
|
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
||||||
|
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
||||||
|
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">2</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
||||||
|
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
||||||
|
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
||||||
|
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
||||||
|
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="bool" key="AutoTest.ApplyFilter">false</value>
|
||||||
|
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
||||||
|
<valuelist type="QVariantList" key="AutoTest.PathFilters"/>
|
||||||
|
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
||||||
|
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
||||||
|
<valuemap type="QVariantMap" key="ClangTools">
|
||||||
|
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
||||||
|
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
||||||
|
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
||||||
|
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
||||||
|
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
||||||
|
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.Target.0</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<value type="QString" key="DeviceType">Desktop</value>
|
||||||
|
<value type="bool" key="HasPerBcDcs">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{78ff90a3-f672-45c2-ad08-343b0923896f}</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
||||||
|
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||||
|
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||||
|
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
||||||
|
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
||||||
|
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||||
|
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||||
|
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||||
|
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||||
|
-DCMAKE_BUILD_TYPE:STRING=Release
|
||||||
|
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build/</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">all</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">clean</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Release</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString"></value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||||
|
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||||
|
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||||
|
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||||
|
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||||
|
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||||
|
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||||
|
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||||
|
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||||
|
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.1">
|
||||||
|
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||||
|
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||||
|
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
||||||
|
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
||||||
|
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||||
|
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||||
|
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||||
|
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||||
|
-DCMAKE_BUILD_TYPE:STRING=Debug
|
||||||
|
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
||||||
|
<value type="QString" key="CMake.Source.Directory">/mnt/share/torsten/Programs/yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">all</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">clean</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Debug (importiert)</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">-1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">install</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||||
|
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">0</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">2</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString"></value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||||
|
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||||
|
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||||
|
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||||
|
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||||
|
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||||
|
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||||
|
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||||
|
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||||
|
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||||
|
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.TargetCount</variable>
|
||||||
|
<value type="qlonglong">1</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
||||||
|
<value type="int">22</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>Version</variable>
|
||||||
|
<value type="int">22</value>
|
||||||
|
</data>
|
||||||
|
</qtcreator>
|
||||||
205
CMakeLists.txt.user.d36652f
Normal file
205
CMakeLists.txt.user.d36652f
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE QtCreatorProject>
|
||||||
|
<!-- Written by QtCreator 12.0.2, 2025-07-18T07:45:58. -->
|
||||||
|
<qtcreator>
|
||||||
|
<data>
|
||||||
|
<variable>EnvironmentId</variable>
|
||||||
|
<value type="QByteArray">{d36652ff-969b-426b-a63f-1edd325096c5}</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
||||||
|
<value type="qlonglong">0</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.AutoSpacesForTabs">false</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
||||||
|
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
||||||
|
<value type="QString" key="language">Cpp</value>
|
||||||
|
<valuemap type="QVariantMap" key="value">
|
||||||
|
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
||||||
|
<value type="QString" key="language">QmlJS</value>
|
||||||
|
<valuemap type="QVariantMap" key="value">
|
||||||
|
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
||||||
|
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
||||||
|
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
||||||
|
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">0</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
||||||
|
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
||||||
|
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
||||||
|
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
||||||
|
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
||||||
|
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
||||||
|
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
||||||
|
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
||||||
|
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
||||||
|
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
||||||
|
<valuemap type="QVariantMap" key="ClangTools">
|
||||||
|
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
||||||
|
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
||||||
|
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
||||||
|
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
||||||
|
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
||||||
|
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
||||||
|
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="CppEditor.QuickFix">
|
||||||
|
<value type="bool" key="UseGlobalSettings">true</value>
|
||||||
|
</valuemap>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.Target.0</variable>
|
||||||
|
<valuemap type="QVariantMap">
|
||||||
|
<value type="QString" key="DeviceType">Desktop</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{3c6cfc13-714d-4db1-bd45-b9794643cc67}</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
||||||
|
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||||
|
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||||
|
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||||
|
-DCMAKE_BUILD_TYPE:STRING=Build
|
||||||
|
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||||
|
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}
|
||||||
|
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||||
|
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||||
|
-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}</value>
|
||||||
|
<value type="QString" key="CMake.Source.Directory">/home/torsten/Programs/yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">all</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||||
|
</valuemap>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||||
|
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||||
|
<value type="QString">clean</value>
|
||||||
|
</valuelist>
|
||||||
|
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||||
|
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||||
|
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">1</value>
|
||||||
|
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||||
|
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||||
|
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||||
|
<value type="QString" key="Analyzer.Valgrind.ValgrindExecutable">/usr/bin/valgrind</value>
|
||||||
|
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||||
|
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||||
|
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||||
|
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.yourpart-daemon</value>
|
||||||
|
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||||
|
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||||
|
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||||
|
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||||
|
</valuemap>
|
||||||
|
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||||
|
</valuemap>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.TargetCount</variable>
|
||||||
|
<value type="qlonglong">1</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
||||||
|
<value type="int">22</value>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<variable>Version</variable>
|
||||||
|
<value type="int">22</value>
|
||||||
|
</data>
|
||||||
|
</qtcreator>
|
||||||
230
PERFORMANCE_ANALYSIS.md
Normal file
230
PERFORMANCE_ANALYSIS.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Backend Performance-Analyse: Sell-Funktionen
|
||||||
|
|
||||||
|
## Identifizierte Performance-Probleme
|
||||||
|
|
||||||
|
### 1. **N+1 Query Problem in `sellAllProducts()`**
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
Die Funktion `sellAllProducts()` macht für jedes Inventory-Item mehrere separate Datenbankabfragen:
|
||||||
|
|
||||||
|
1. **Erste Schleife (Zeile 1702-1711):**
|
||||||
|
- `calcRegionalSellPrice()` → macht `TownProductWorth.findOne()` für jedes Item
|
||||||
|
- `getCumulativeTaxPercentWithExemptions()` → macht mehrere Queries pro Item:
|
||||||
|
- `FalukantCharacter.findOne()`
|
||||||
|
- `PoliticalOffice.findAll()` mit Includes
|
||||||
|
- Rekursive SQL-Query für Steuerberechnung
|
||||||
|
- `addSellItem()` → macht `Branch.findOne()` und `DaySell.findOne()`/`create()` für jedes Item
|
||||||
|
|
||||||
|
2. **Zweite Schleife (Zeile 1714-1724):**
|
||||||
|
- `RegionData.findOne()` für jedes Item
|
||||||
|
- `getCumulativeTaxPercent()` → rekursive SQL-Query für jedes Item
|
||||||
|
- `calcRegionalSellPrice()` → erneut `TownProductWorth.findOne()` für jedes Item
|
||||||
|
|
||||||
|
**Beispiel:** Bei 10 Items werden gemacht:
|
||||||
|
- 10x `TownProductWorth.findOne()` (2x pro Item)
|
||||||
|
- 10x `RegionData.findOne()`
|
||||||
|
- 10x `getCumulativeTaxPercentWithExemptions()` (mit mehreren Queries)
|
||||||
|
- 10x `getCumulativeTaxPercent()` (rekursive SQL)
|
||||||
|
- 10x `addSellItem()` (mit 2 Queries pro Item)
|
||||||
|
- = **~70+ Datenbankabfragen für 10 Items**
|
||||||
|
|
||||||
|
### 2. **Ineffiziente `addSellItem()` Implementierung**
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- Wird für jedes Item einzeln aufgerufen
|
||||||
|
- Macht `Branch.findOne()` für jedes Item (könnte gecacht werden)
|
||||||
|
- `DaySell.findOne()` und `create()`/`update()` für jedes Item
|
||||||
|
|
||||||
|
**Lösung:** Batch-Operation implementieren, die alle DaySell Einträge auf einmal verarbeitet.
|
||||||
|
|
||||||
|
### 3. **Doppelte Berechnungen in `sellAllProducts()`**
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- Preis wird zweimal berechnet (Zeile 1705 und 1718)
|
||||||
|
- Steuer wird zweimal berechnet (Zeile 1706 und 1717)
|
||||||
|
- `calcRegionalSellPrice()` wird zweimal aufgerufen mit denselben Parametern
|
||||||
|
|
||||||
|
### 4. **Fehlende Indizes**
|
||||||
|
|
||||||
|
**Potenzielle fehlende Indizes:**
|
||||||
|
- `falukant_data.town_product_worth(product_id, region_id)` - sollte unique sein
|
||||||
|
- `falukant_data.inventory(stock_id, product_id, quality)` - für schnelle Lookups
|
||||||
|
- `falukant_data.knowledge(character_id, product_id)` - für Knowledge-Lookups
|
||||||
|
- `falukant_data.political_office(character_id)` - für Steuerbefreiungen
|
||||||
|
|
||||||
|
### 5. **Ineffiziente `getCumulativeTaxPercentWithExemptions()`**
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- Lädt alle PoliticalOffices jedes Mal neu, auch wenn sich nichts geändert hat
|
||||||
|
- Macht komplexe rekursive SQL-Query für jedes Item separat
|
||||||
|
- Könnte gecacht werden (z.B. pro User+Region Kombination)
|
||||||
|
|
||||||
|
## Empfohlene Optimierungen
|
||||||
|
|
||||||
|
### 1. **Batch-Loading für `sellAllProducts()`**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async sellAllProducts(hashedUserId, branchId) {
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
// Batch-Load alle benötigten Daten VOR den Schleifen
|
||||||
|
const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))];
|
||||||
|
const productIds = [...new Set(inventory.map(item => item.productType.id))];
|
||||||
|
|
||||||
|
// 1. Lade alle TownProductWorth Einträge auf einmal
|
||||||
|
const townWorths = await TownProductWorth.findAll({
|
||||||
|
where: {
|
||||||
|
productId: { [Op.in]: productIds },
|
||||||
|
regionId: { [Op.in]: regionIds }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const worthMap = new Map();
|
||||||
|
townWorths.forEach(tw => {
|
||||||
|
worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Lade alle RegionData auf einmal
|
||||||
|
const regions = await RegionData.findAll({
|
||||||
|
where: { id: { [Op.in]: regionIds } }
|
||||||
|
});
|
||||||
|
const regionMap = new Map(regions.map(r => [r.id, r]));
|
||||||
|
|
||||||
|
// 3. Berechne Steuern für alle Regionen auf einmal
|
||||||
|
const taxMap = new Map();
|
||||||
|
for (const regionId of regionIds) {
|
||||||
|
const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
|
||||||
|
taxMap.set(regionId, tax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Berechne Preise und Steuern in einer Schleife
|
||||||
|
const sellItems = [];
|
||||||
|
for (const item of inventory) {
|
||||||
|
const regionId = item.stock.branch.regionId;
|
||||||
|
const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;
|
||||||
|
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
|
||||||
|
const pricePerUnit = calcRegionalSellPrice(item.productType, knowledgeVal, regionId, worthPercent);
|
||||||
|
const cumulativeTax = taxMap.get(regionId);
|
||||||
|
// ... rest of calculation ...
|
||||||
|
sellItems.push({ branchId: item.stock.branch.id, productId: item.productType.id, quantity: item.quantity });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Batch-Update DaySell Einträge
|
||||||
|
await this.addSellItemsBatch(sellItems);
|
||||||
|
|
||||||
|
// ... rest of code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Batch-Operation für `addSellItem()`**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async addSellItemsBatch(sellItems) {
|
||||||
|
// Gruppiere nach (regionId, productId, sellerId)
|
||||||
|
const grouped = new Map();
|
||||||
|
for (const item of sellItems) {
|
||||||
|
const branch = await Branch.findByPk(item.branchId);
|
||||||
|
if (!branch) continue;
|
||||||
|
|
||||||
|
const key = `${branch.regionId}-${item.productId}-${item.sellerId}`;
|
||||||
|
if (!grouped.has(key)) {
|
||||||
|
grouped.set(key, {
|
||||||
|
regionId: branch.regionId,
|
||||||
|
productId: item.productId,
|
||||||
|
sellerId: item.sellerId,
|
||||||
|
quantity: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
grouped.get(key).quantity += item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-Update oder Create
|
||||||
|
for (const [key, data] of grouped) {
|
||||||
|
const [daySell, created] = await DaySell.findOrCreate({
|
||||||
|
where: {
|
||||||
|
regionId: data.regionId,
|
||||||
|
productId: data.productId,
|
||||||
|
sellerId: data.sellerId
|
||||||
|
},
|
||||||
|
defaults: { quantity: data.quantity }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!created) {
|
||||||
|
daySell.quantity += data.quantity;
|
||||||
|
await daySell.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Caching für `getCumulativeTaxPercentWithExemptions()`**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Cache für Steuerberechnungen (z.B. 5 Minuten)
|
||||||
|
const taxCache = new Map();
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten
|
||||||
|
|
||||||
|
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
|
||||||
|
const cacheKey = `${userId}-${regionId}`;
|
||||||
|
const cached = taxCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing calculation ...
|
||||||
|
|
||||||
|
taxCache.set(cacheKey, { value: tax, timestamp: Date.now() });
|
||||||
|
return tax;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Optimierte `calcRegionalSellPrice()`**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||||
|
// Wenn worthPercent nicht übergeben wurde UND wir es nicht aus dem Cache haben,
|
||||||
|
// dann hole es aus der DB
|
||||||
|
if (worthPercent === null) {
|
||||||
|
const townWorth = await TownProductWorth.findOne({
|
||||||
|
where: { productId: product.id, regionId: regionId }
|
||||||
|
});
|
||||||
|
worthPercent = townWorth?.worthPercent || 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of calculation ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Datenbank-Indizes hinzufügen**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Index für town_product_worth (sollte unique sein)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_town_product_worth_product_region
|
||||||
|
ON falukant_data.town_product_worth(product_id, region_id);
|
||||||
|
|
||||||
|
-- Index für inventory Lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inventory_stock_product_quality
|
||||||
|
ON falukant_data.inventory(stock_id, product_id, quality);
|
||||||
|
|
||||||
|
-- Index für knowledge Lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_knowledge_character_product
|
||||||
|
ON falukant_data.knowledge(character_id, product_id);
|
||||||
|
|
||||||
|
-- Index für political_office Lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_political_office_character
|
||||||
|
ON falukant_data.political_office(character_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Geschätzter Performance-Gewinn
|
||||||
|
|
||||||
|
- **Vorher:** ~70+ Queries für 10 Items
|
||||||
|
- **Nachher:** ~15-20 Queries für 10 Items (Batch-Loading + Caching)
|
||||||
|
- **Geschätzte Verbesserung:** 70-80% weniger Datenbankabfragen
|
||||||
|
|
||||||
|
## Priorität
|
||||||
|
|
||||||
|
1. **Hoch:** Batch-Loading für `sellAllProducts()` (größter Impact)
|
||||||
|
2. **Hoch:** Batch-Operation für `addSellItem()`
|
||||||
|
3. **Mittel:** Caching für Steuerberechnungen
|
||||||
|
4. **Mittel:** Datenbank-Indizes
|
||||||
|
5. **Niedrig:** Doppelte Berechnungen entfernen
|
||||||
|
|
||||||
601
SELL_OVERVIEW.md
Normal file
601
SELL_OVERVIEW.md
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
# Übersicht: Sell-Funktionen und verwendete Models/Tabellen
|
||||||
|
|
||||||
|
## Sell-Funktionen in `falukantService.js`
|
||||||
|
|
||||||
|
### 1. `sellProduct(hashedUserId, branchId, productId, quality, quantity)`
|
||||||
|
Verkauft ein einzelnes Produkt mit bestimmter Qualität.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Lädt User, Branch, Character, Stock
|
||||||
|
2. Lädt Inventory mit ProductType und Knowledge
|
||||||
|
3. Berechnet Preis pro Einheit mit `calcRegionalSellPrice()`
|
||||||
|
4. Berechnet kumulative Steuer mit politischen Befreiungen
|
||||||
|
5. Passt Preis an (Inflation basierend auf Steuer)
|
||||||
|
6. Berechnet Revenue, Tax, Net
|
||||||
|
7. Aktualisiert Geld für Verkäufer und Treasury
|
||||||
|
8. Entfernt verkaufte Items aus Inventory
|
||||||
|
9. Erstellt/aktualisiert DaySell Eintrag
|
||||||
|
10. Sendet Socket-Notifications
|
||||||
|
|
||||||
|
**Verwendete Models/Tabellen:**
|
||||||
|
- `FalukantUser` (`falukant_data.falukant_user`)
|
||||||
|
- `Branch` (`falukant_data.branch`)
|
||||||
|
- `FalukantCharacter` (`falukant_data.character`)
|
||||||
|
- `FalukantStock` (`falukant_data.stock`)
|
||||||
|
- `Inventory` (`falukant_data.inventory`)
|
||||||
|
- `ProductType` (`falukant_type.product`)
|
||||||
|
- `Knowledge` (`falukant_data.knowledge`)
|
||||||
|
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||||
|
- `RegionData` (`falukant_data.region`)
|
||||||
|
- `RegionType` (`falukant_type.region`)
|
||||||
|
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||||
|
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||||
|
- `DaySell` (`falukant_log.day_sell`)
|
||||||
|
- `MoneyFlow` (via `updateFalukantUserMoney`)
|
||||||
|
|
||||||
|
### 2. `sellAllProducts(hashedUserId, branchId)`
|
||||||
|
Verkauft alle Produkte eines Branches.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Lädt User, Branch mit Stocks
|
||||||
|
2. Lädt alle Inventory Items mit ProductType, Knowledge, Stock, Branch
|
||||||
|
3. Für jedes Item:
|
||||||
|
- Berechnet Preis pro Einheit
|
||||||
|
- Berechnet kumulative Steuer
|
||||||
|
- Passt Preis an
|
||||||
|
- Erstellt/aktualisiert DaySell Eintrag
|
||||||
|
4. Berechnet Gesamt-Tax pro Region
|
||||||
|
5. Aktualisiert Geld für Verkäufer und Treasury
|
||||||
|
6. Löscht alle Inventory Items
|
||||||
|
7. Sendet Socket-Notifications
|
||||||
|
|
||||||
|
**Verwendete Models/Tabellen:**
|
||||||
|
- `FalukantUser` (`falukant_data.falukant_user`)
|
||||||
|
- `Branch` (`falukant_data.branch`)
|
||||||
|
- `FalukantStock` (`falukant_data.stock`)
|
||||||
|
- `FalukantStockType` (`falukant_type.stock`)
|
||||||
|
- `FalukantCharacter` (`falukant_data.character`)
|
||||||
|
- `Inventory` (`falukant_data.inventory`)
|
||||||
|
- `ProductType` (`falukant_type.product`)
|
||||||
|
- `Knowledge` (`falukant_data.knowledge`)
|
||||||
|
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||||
|
- `RegionData` (`falukant_data.region`)
|
||||||
|
- `RegionType` (`falukant_type.region`)
|
||||||
|
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||||
|
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||||
|
- `DaySell` (`falukant_log.day_sell`)
|
||||||
|
- `MoneyFlow` (via `updateFalukantUserMoney`)
|
||||||
|
|
||||||
|
### 3. `addSellItem(branchId, userId, productId, quantity)`
|
||||||
|
Erstellt oder aktualisiert einen DaySell Eintrag für einen Verkauf.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Lädt Branch
|
||||||
|
2. Sucht nach existierendem DaySell Eintrag
|
||||||
|
3. Erstellt neuen oder aktualisiert existierenden Eintrag
|
||||||
|
|
||||||
|
**Verwendete Models/Tabellen:**
|
||||||
|
- `Branch` (`falukant_data.branch`)
|
||||||
|
- `DaySell` (`falukant_log.day_sell`)
|
||||||
|
|
||||||
|
## Hilfsfunktionen
|
||||||
|
|
||||||
|
### `calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null)`
|
||||||
|
Berechnet den Verkaufspreis eines Produkts basierend auf:
|
||||||
|
- Basispreis (`product.sellCost`)
|
||||||
|
- Regionalem Worth-Percent (`town_product_worth.worth_percent`)
|
||||||
|
- Knowledge-Faktor (0-100)
|
||||||
|
|
||||||
|
**Verwendete Models/Tabellen:**
|
||||||
|
- `ProductType` (`falukant_type.product`)
|
||||||
|
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||||
|
|
||||||
|
### `getCumulativeTaxPercent(regionId)`
|
||||||
|
Berechnet die kumulative Steuer für eine Region und alle Vorfahren (rekursiv).
|
||||||
|
|
||||||
|
**SQL Query:**
|
||||||
|
```sql
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT id, parent_id, tax_percent
|
||||||
|
FROM falukant_data.region r
|
||||||
|
WHERE id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT reg.id, reg.parent_id, reg.tax_percent
|
||||||
|
FROM falukant_data.region reg
|
||||||
|
JOIN ancestors a ON reg.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendete Tabellen:**
|
||||||
|
- `falukant_data.region`
|
||||||
|
|
||||||
|
### `getCumulativeTaxPercentWithExemptions(userId, regionId)`
|
||||||
|
Berechnet die kumulative Steuer mit politischen Befreiungen.
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
1. Lädt Character des Users
|
||||||
|
2. Lädt alle PoliticalOffices des Characters
|
||||||
|
3. Bestimmt befreite Region-Typen basierend auf Ämtern
|
||||||
|
4. Berechnet kumulative Steuer, aber schließt befreite Region-Typen aus
|
||||||
|
|
||||||
|
**SQL Query:**
|
||||||
|
```sql
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
|
||||||
|
FROM falukant_data.region r
|
||||||
|
JOIN falukant_type.region rt ON rt.id = r.region_type_id
|
||||||
|
WHERE r.id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
|
||||||
|
FROM falukant_data.region reg
|
||||||
|
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
|
||||||
|
JOIN ancestors a ON reg.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT COALESCE(SUM(CASE WHEN ARRAY[...] && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendete Models/Tabellen:**
|
||||||
|
- `FalukantCharacter` (`falukant_data.character`)
|
||||||
|
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||||
|
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||||
|
- `RegionData` (`falukant_data.region`)
|
||||||
|
- `RegionType` (`falukant_type.region`)
|
||||||
|
|
||||||
|
**Politische Steuerbefreiungen:**
|
||||||
|
```javascript
|
||||||
|
const POLITICAL_TAX_EXEMPTIONS = {
|
||||||
|
'council': ['city'],
|
||||||
|
'taxman': ['city', 'county'],
|
||||||
|
'treasurerer': ['city', 'county', 'shire'],
|
||||||
|
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
|
||||||
|
'chancellor': ['city','county','shire','markgrave','duchy'] // = alle Typen
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model-Definitionen
|
||||||
|
|
||||||
|
### Inventory (`falukant_data.inventory`)
|
||||||
|
```javascript
|
||||||
|
// backend/models/falukant/data/inventory.js
|
||||||
|
- id
|
||||||
|
- stockId (FK zu falukant_data.stock)
|
||||||
|
- productId (FK zu falukant_type.product)
|
||||||
|
- quantity
|
||||||
|
- quality
|
||||||
|
- producedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
### DaySell (`falukant_log.day_sell`)
|
||||||
|
```javascript
|
||||||
|
// backend/models/falukant/log/daysell.js
|
||||||
|
- id
|
||||||
|
- regionId (FK zu falukant_data.region)
|
||||||
|
- productId (FK zu falukant_type.product)
|
||||||
|
- sellerId (FK zu falukant_data.falukant_user)
|
||||||
|
- quantity
|
||||||
|
- createdAt
|
||||||
|
- updatedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
### TownProductWorth (`falukant_data.town_product_worth`)
|
||||||
|
```javascript
|
||||||
|
// backend/models/falukant/data/town_product_worth.js
|
||||||
|
- id
|
||||||
|
- productId (FK zu falukant_type.product)
|
||||||
|
- regionId (FK zu falukant_data.region)
|
||||||
|
- worthPercent (0-100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Knowledge (`falukant_data.knowledge`)
|
||||||
|
```javascript
|
||||||
|
// backend/models/falukant/data/product_knowledge.js
|
||||||
|
- id
|
||||||
|
- productId (FK zu falukant_type.product)
|
||||||
|
- characterId (FK zu falukant_data.character)
|
||||||
|
- knowledge (0-99)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige SQL-Queries
|
||||||
|
|
||||||
|
### 1. Inventory mit ProductType und Knowledge laden
|
||||||
|
```javascript
|
||||||
|
Inventory.findAll({
|
||||||
|
where: { quality },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ProductType,
|
||||||
|
as: 'productType',
|
||||||
|
required: true,
|
||||||
|
where: { id: productId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Knowledge,
|
||||||
|
as: 'knowledges',
|
||||||
|
required: false,
|
||||||
|
where: { characterId: character.id }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Kumulative Steuer mit Befreiungen berechnen
|
||||||
|
Siehe `getCumulativeTaxPercentWithExemptions()` oben.
|
||||||
|
|
||||||
|
## Preisberechnung
|
||||||
|
|
||||||
|
### Formel für `calcRegionalSellPrice`:
|
||||||
|
1. Basispreis = `product.sellCost * (worthPercent / 100)`
|
||||||
|
2. Min = `basePrice * 0.6`
|
||||||
|
3. Max = `basePrice`
|
||||||
|
4. Preis = `min + (max - min) * (knowledgeFactor / 100)`
|
||||||
|
|
||||||
|
### Steueranpassung:
|
||||||
|
1. `inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100))`
|
||||||
|
2. `adjustedPricePerUnit = pricePerUnit * inflationFactor`
|
||||||
|
3. `revenue = quantity * adjustedPricePerUnit`
|
||||||
|
4. `taxValue = revenue * cumulativeTax / 100`
|
||||||
|
5. `net = revenue - taxValue`
|
||||||
|
|
||||||
|
## Vollständige Code-Snippets
|
||||||
|
|
||||||
|
### `calcRegionalSellPrice()`
|
||||||
|
```javascript
|
||||||
|
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||||
|
if (worthPercent === null) {
|
||||||
|
const townWorth = await TownProductWorth.findOne({
|
||||||
|
where: { productId: product.id, regionId: regionId }
|
||||||
|
});
|
||||||
|
worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basispreis basierend auf regionalem worthPercent
|
||||||
|
const basePrice = product.sellCost * (worthPercent / 100);
|
||||||
|
|
||||||
|
// Dann Knowledge-Faktor anwenden
|
||||||
|
const min = basePrice * 0.6;
|
||||||
|
const max = basePrice;
|
||||||
|
return min + (max - min) * (knowledgeFactor / 100);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getCumulativeTaxPercent()`
|
||||||
|
```javascript
|
||||||
|
async function getCumulativeTaxPercent(regionId) {
|
||||||
|
if (!regionId) return 0;
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
`WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT id, parent_id, tax_percent
|
||||||
|
FROM falukant_data.region r
|
||||||
|
WHERE id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT reg.id, reg.parent_id, reg.tax_percent
|
||||||
|
FROM falukant_data.region reg
|
||||||
|
JOIN ancestors a ON reg.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
|
||||||
|
{
|
||||||
|
replacements: { id: regionId },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const val = rows?.[0]?.total ?? 0;
|
||||||
|
return parseFloat(val) || 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getCumulativeTaxPercentWithExemptions()` (vereinfacht)
|
||||||
|
```javascript
|
||||||
|
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
|
||||||
|
if (!regionId) return 0;
|
||||||
|
|
||||||
|
// Character finden
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
if (!character) return 0;
|
||||||
|
|
||||||
|
// Politische Ämter laden
|
||||||
|
const offices = await PoliticalOffice.findAll({
|
||||||
|
where: { characterId: character.id },
|
||||||
|
include: [
|
||||||
|
{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] },
|
||||||
|
{
|
||||||
|
model: RegionData,
|
||||||
|
as: 'region',
|
||||||
|
include: [{
|
||||||
|
model: RegionType,
|
||||||
|
as: 'regionType',
|
||||||
|
attributes: ['labelTr']
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Befreite Region-Typen bestimmen
|
||||||
|
const exemptTypes = new Set();
|
||||||
|
let hasChancellor = false;
|
||||||
|
for (const o of offices) {
|
||||||
|
const name = o.type?.name;
|
||||||
|
if (!name) continue;
|
||||||
|
if (name === 'chancellor') { hasChancellor = true; break; }
|
||||||
|
const allowed = POLITICAL_TAX_EXEMPTIONS[name];
|
||||||
|
if (allowed && Array.isArray(allowed)) {
|
||||||
|
for (const t of allowed) exemptTypes.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChancellor) return 0;
|
||||||
|
|
||||||
|
// SQL Query mit Befreiungen
|
||||||
|
const exemptTypesArray = Array.from(exemptTypes);
|
||||||
|
const exemptTypesString = exemptTypesArray.length > 0
|
||||||
|
? `ARRAY[${exemptTypesArray.map(t => `'${t.replace(/'/g, "''")}'`).join(',')}]`
|
||||||
|
: `ARRAY[]::text[]`;
|
||||||
|
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
`WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
|
||||||
|
FROM falukant_data.region r
|
||||||
|
JOIN falukant_type.region rt ON rt.id = r.region_type_id
|
||||||
|
WHERE r.id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
|
||||||
|
FROM falukant_data.region reg
|
||||||
|
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
|
||||||
|
JOIN ancestors a ON reg.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT COALESCE(SUM(CASE WHEN ${exemptTypesString} && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
|
||||||
|
{
|
||||||
|
replacements: { id: regionId },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const val = rows?.[0]?.total ?? 0;
|
||||||
|
return parseFloat(val) || 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sellProduct()` (Kern-Logik)
|
||||||
|
```javascript
|
||||||
|
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
|
||||||
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
const branch = await getBranchOrFail(user.id, branchId);
|
||||||
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||||
|
if (!character) throw new Error('No character found for user');
|
||||||
|
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
|
||||||
|
if (!stock) throw new Error('Stock not found');
|
||||||
|
|
||||||
|
// Inventory laden
|
||||||
|
const inventory = await Inventory.findAll({
|
||||||
|
where: { quality },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ProductType,
|
||||||
|
as: 'productType',
|
||||||
|
required: true,
|
||||||
|
where: { id: productId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Knowledge,
|
||||||
|
as: 'knowledges',
|
||||||
|
required: false,
|
||||||
|
where: { characterId: character.id }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (!inventory.length) throw new Error('No inventory found');
|
||||||
|
|
||||||
|
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
||||||
|
if (available < quantity) throw new Error('Not enough inventory available');
|
||||||
|
|
||||||
|
const item = inventory[0].productType;
|
||||||
|
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
||||||
|
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
||||||
|
|
||||||
|
// Steuer berechnen
|
||||||
|
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
|
||||||
|
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
||||||
|
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
||||||
|
const revenue = quantity * adjustedPricePerUnit;
|
||||||
|
|
||||||
|
// Tax und Net berechnen
|
||||||
|
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
|
||||||
|
const net = Math.round((revenue - taxValue) * 100) / 100;
|
||||||
|
|
||||||
|
// Geld aktualisieren
|
||||||
|
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
|
||||||
|
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
||||||
|
|
||||||
|
// Steuer an Treasury buchen
|
||||||
|
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
||||||
|
if (treasuryId && taxValue > 0) {
|
||||||
|
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
|
||||||
|
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory aktualisieren
|
||||||
|
let remaining = quantity;
|
||||||
|
for (const inv of inventory) {
|
||||||
|
if (inv.quantity <= remaining) {
|
||||||
|
remaining -= inv.quantity;
|
||||||
|
await inv.destroy();
|
||||||
|
} else {
|
||||||
|
await inv.update({ quantity: inv.quantity - remaining });
|
||||||
|
remaining = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaySell Eintrag erstellen/aktualisieren
|
||||||
|
await this.addSellItem(branchId, user.id, productId, quantity);
|
||||||
|
|
||||||
|
// Notifications senden
|
||||||
|
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
|
||||||
|
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `addSellItem()`
|
||||||
|
```javascript
|
||||||
|
async addSellItem(branchId, userId, productId, quantity) {
|
||||||
|
const branch = await Branch.findOne({
|
||||||
|
where: { id: branchId },
|
||||||
|
});
|
||||||
|
const daySell = await DaySell.findOne({
|
||||||
|
where: {
|
||||||
|
regionId: branch.regionId,
|
||||||
|
productId: productId,
|
||||||
|
sellerId: userId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (daySell) {
|
||||||
|
daySell.quantity += quantity;
|
||||||
|
await daySell.save();
|
||||||
|
} else {
|
||||||
|
await DaySell.create({
|
||||||
|
regionId: branch.regionId,
|
||||||
|
productId: productId,
|
||||||
|
sellerId: userId,
|
||||||
|
quantity: quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
|
||||||
|
1. **Inventory wird nach Verkauf gelöscht/aktualisiert**: Items werden aus der Inventory entfernt oder die Menge reduziert.
|
||||||
|
|
||||||
|
2. **DaySell wird aggregiert**: Wenn bereits ein DaySell Eintrag für Region/Product/Seller existiert, wird die Menge addiert.
|
||||||
|
|
||||||
|
3. **Steuer wird an Treasury gebucht**: Wenn `TREASURY_FALUKANT_USER_ID` gesetzt ist, wird die Steuer an diesen User gebucht.
|
||||||
|
|
||||||
|
4. **Socket-Notifications**: Nach jedem Verkauf werden `falukantUpdateStatus` und `falukantBranchUpdate` Events gesendet.
|
||||||
|
|
||||||
|
5. **Politische Befreiungen**: Bestimmte politische Ämter befreien von Steuern in bestimmten Region-Typen. Chancellor befreit von allen Steuern.
|
||||||
|
|
||||||
|
6. **Preis-Inflation**: Der Preis wird basierend auf der Steuer inflatiert, damit der Netto-Betrag für den Verkäufer gleich bleibt.
|
||||||
|
|
||||||
|
## Tabellenübersicht
|
||||||
|
|
||||||
|
### `falukant_data.inventory`
|
||||||
|
- `id` (PK)
|
||||||
|
- `stock_id` (FK zu `falukant_data.stock`)
|
||||||
|
- `product_id` (FK zu `falukant_type.product`)
|
||||||
|
- `quantity` (INTEGER)
|
||||||
|
- `quality` (INTEGER)
|
||||||
|
- `produced_at` (DATE)
|
||||||
|
|
||||||
|
### `falukant_log.sell` (DaySell)
|
||||||
|
- `id` (PK)
|
||||||
|
- `region_id` (FK zu `falukant_data.region`)
|
||||||
|
- `product_id` (FK zu `falukant_type.product`)
|
||||||
|
- `seller_id` (FK zu `falukant_data.falukant_user`)
|
||||||
|
- `quantity` (INTEGER)
|
||||||
|
- `sell_timestamp` (DATE)
|
||||||
|
- **Unique Index**: `(seller_id, product_id, region_id)`
|
||||||
|
|
||||||
|
### `falukant_data.town_product_worth`
|
||||||
|
- `id` (PK)
|
||||||
|
- `product_id` (FK zu `falukant_type.product`)
|
||||||
|
- `region_id` (FK zu `falukant_data.region`)
|
||||||
|
- `worth_percent` (INTEGER, 0-100)
|
||||||
|
|
||||||
|
### `falukant_data.knowledge`
|
||||||
|
- `id` (PK)
|
||||||
|
- `product_id` (FK zu `falukant_type.product`)
|
||||||
|
- `character_id` (FK zu `falukant_data.character`)
|
||||||
|
- `knowledge` (INTEGER, 0-99)
|
||||||
|
|
||||||
|
### `falukant_data.political_office`
|
||||||
|
- `id` (PK)
|
||||||
|
- `office_type_id` (FK zu `falukant_type.political_office_type`)
|
||||||
|
- `character_id` (FK zu `falukant_data.character`)
|
||||||
|
- `region_id` (FK zu `falukant_data.region`)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
### `falukant_type.political_office_type`
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (STRING) - z.B. 'council', 'taxman', 'treasurerer', 'super-state-administrator', 'chancellor'
|
||||||
|
- `seats_per_region` (INTEGER)
|
||||||
|
- `region_type` (STRING)
|
||||||
|
- `term_length` (INTEGER)
|
||||||
|
|
||||||
|
### `falukant_data.region`
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (STRING)
|
||||||
|
- `region_type_id` (FK zu `falukant_type.region`)
|
||||||
|
- `parent_id` (FK zu `falukant_data.region`, nullable)
|
||||||
|
- `map` (JSONB)
|
||||||
|
- `tax_percent` (DECIMAL)
|
||||||
|
|
||||||
|
### `falukant_type.region`
|
||||||
|
- `id` (PK)
|
||||||
|
- `label_tr` (STRING) - z.B. 'city', 'county', 'shire', 'markgrave', 'duchy'
|
||||||
|
- `parent_id` (FK zu `falukant_type.region`, nullable)
|
||||||
|
|
||||||
|
### `falukant_data.falukant_user`
|
||||||
|
- `id` (PK)
|
||||||
|
- `user_id` (FK zu `community.user`)
|
||||||
|
- `money` (DECIMAL)
|
||||||
|
- `credit_amount`, `today_credit_taken`, `credit_interest_rate`
|
||||||
|
- `certificate`
|
||||||
|
- `main_branch_region_id`
|
||||||
|
- `last_nobility_advance_at`
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
### `falukant_data.character`
|
||||||
|
- `id` (PK)
|
||||||
|
- `user_id` (FK zu `falukant_data.falukant_user`)
|
||||||
|
- `region_id` (FK zu `falukant_data.region`)
|
||||||
|
- `first_name`, `last_name`
|
||||||
|
- `birthdate`, `gender`, `health`
|
||||||
|
- `title_of_nobility` (FK zu `falukant_type.title_of_nobility`)
|
||||||
|
- `mood_id` (FK zu `falukant_type.mood`)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
### `falukant_data.branch`
|
||||||
|
- `id` (PK)
|
||||||
|
- `branch_type_id` (FK zu `falukant_type.branch`)
|
||||||
|
- `region_id` (FK zu `falukant_data.region`)
|
||||||
|
- `falukant_user_id` (FK zu `falukant_data.falukant_user`)
|
||||||
|
|
||||||
|
### `falukant_data.stock`
|
||||||
|
- `id` (PK)
|
||||||
|
- `branch_id` (FK zu `falukant_data.branch`)
|
||||||
|
- `stock_type_id` (FK zu `falukant_type.stock`)
|
||||||
|
- `quantity` (INTEGER)
|
||||||
|
- `product_quality` (INTEGER, nullable)
|
||||||
|
|
||||||
|
### `falukant_type.product`
|
||||||
|
- `id` (PK)
|
||||||
|
- `label_tr` (STRING, unique)
|
||||||
|
- `category` (INTEGER)
|
||||||
|
- `production_time` (INTEGER)
|
||||||
|
- `sell_cost` (INTEGER)
|
||||||
|
|
||||||
|
## Dateipfade
|
||||||
|
|
||||||
|
- **Service**: `backend/services/falukantService.js`
|
||||||
|
- **Models**:
|
||||||
|
- `backend/models/falukant/data/inventory.js`
|
||||||
|
- `backend/models/falukant/log/daysell.js`
|
||||||
|
- `backend/models/falukant/data/town_product_worth.js`
|
||||||
|
- `backend/models/falukant/data/product_knowledge.js`
|
||||||
|
- `backend/models/falukant/data/political_office.js`
|
||||||
|
- `backend/models/falukant/type/political_office_type.js`
|
||||||
|
- `backend/models/falukant/data/region.js`
|
||||||
|
- `backend/models/falukant/type/region.js`
|
||||||
|
- `backend/models/falukant/data/character.js`
|
||||||
|
- `backend/models/falukant/data/user.js`
|
||||||
|
- `backend/models/falukant/data/branch.js`
|
||||||
|
- `backend/models/falukant/data/stock.js`
|
||||||
|
- `backend/models/falukant/type/product.js`
|
||||||
|
|
||||||
168
SSL-SETUP.md
Normal file
168
SSL-SETUP.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# SSL/TLS Setup für YourPart Daemon
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt, wie Sie SSL/TLS-Zertifikate für den YourPart Daemon einrichten können.
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
### 1. Self-Signed Certificate (Entwicklung/Testing)
|
||||||
|
```bash
|
||||||
|
./setup-ssl.sh
|
||||||
|
# Wählen Sie Option 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Let's Encrypt Certificate (Produktion)
|
||||||
|
```bash
|
||||||
|
./setup-ssl.sh
|
||||||
|
# Wählen Sie Option 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Apache2-Zertifikate verwenden (empfohlen für Ubuntu)
|
||||||
|
```bash
|
||||||
|
./setup-ssl.sh
|
||||||
|
# Wählen Sie Option 4
|
||||||
|
# Verwendet bereits vorhandene Apache2-Zertifikate
|
||||||
|
# ⚠️ Warnung bei Snakeoil-Zertifikaten (nur für localhost)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. DNS-01 Challenge (für komplexe Setups)
|
||||||
|
```bash
|
||||||
|
./setup-ssl-dns.sh
|
||||||
|
# Für Cloudflare, Route53, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Voraussetzungen
|
||||||
|
|
||||||
|
### Für Apache2-Zertifikate:
|
||||||
|
- Apache2 installiert oder Zertifikate in Standard-Pfaden
|
||||||
|
- Unterstützte Pfade (priorisiert nach Qualität):
|
||||||
|
- `/etc/letsencrypt/live/your-part.de/fullchain.pem` (Let's Encrypt - empfohlen)
|
||||||
|
- `/etc/letsencrypt/live/$(hostname)/fullchain.pem` (Let's Encrypt)
|
||||||
|
- `/etc/apache2/ssl/apache.crt` (Custom Apache2)
|
||||||
|
- `/etc/ssl/certs/ssl-cert-snakeoil.pem` (Ubuntu Standard - nur localhost)
|
||||||
|
|
||||||
|
### Für Let's Encrypt (HTTP-01 Challenge):
|
||||||
|
- Port 80 muss verfügbar sein
|
||||||
|
- Domain `your-part.de` muss auf den Server zeigen
|
||||||
|
- Kein anderer Service auf Port 80
|
||||||
|
|
||||||
|
### Für DNS-01 Challenge:
|
||||||
|
- DNS-Provider Account (Cloudflare, Route53, etc.)
|
||||||
|
- API-Credentials für DNS-Management
|
||||||
|
|
||||||
|
## 🔧 Konfiguration
|
||||||
|
|
||||||
|
Nach der Zertifikats-Erstellung:
|
||||||
|
|
||||||
|
1. **SSL in der Konfiguration aktivieren:**
|
||||||
|
```ini
|
||||||
|
# /etc/yourpart/daemon.conf
|
||||||
|
WEBSOCKET_SSL_ENABLED=true
|
||||||
|
WEBSOCKET_SSL_CERT_PATH=/etc/yourpart/server.crt
|
||||||
|
WEBSOCKET_SSL_KEY_PATH=/etc/yourpart/server.key
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Daemon neu starten:**
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart yourpart-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verbindung testen:**
|
||||||
|
```bash
|
||||||
|
# WebSocket Secure
|
||||||
|
wss://your-part.de:4551
|
||||||
|
|
||||||
|
# Oder ohne SSL
|
||||||
|
ws://your-part.de:4551
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Automatische Erneuerung
|
||||||
|
|
||||||
|
### Let's Encrypt-Zertifikate:
|
||||||
|
- **Cron Job:** Täglich um 2:30 Uhr
|
||||||
|
- **Script:** `/etc/yourpart/renew-ssl.sh`
|
||||||
|
- **Log:** `/var/log/yourpart/ssl-renewal.log`
|
||||||
|
|
||||||
|
### Apache2-Zertifikate:
|
||||||
|
- **Ubuntu Snakeoil:** Automatisch von Apache2 verwaltet
|
||||||
|
- **Let's Encrypt:** Automatische Erneuerung wenn erkannt
|
||||||
|
- **Custom:** Manuelle Verwaltung erforderlich
|
||||||
|
|
||||||
|
## 📁 Dateistruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
/etc/yourpart/
|
||||||
|
├── server.crt # Zertifikat (Symlink zu Let's Encrypt)
|
||||||
|
├── server.key # Private Key (Symlink zu Let's Encrypt)
|
||||||
|
├── renew-ssl.sh # Auto-Renewal Script
|
||||||
|
└── cloudflare.ini # Cloudflare Credentials (falls verwendet)
|
||||||
|
|
||||||
|
/etc/letsencrypt/live/your-part.de/
|
||||||
|
├── fullchain.pem # Vollständige Zertifikatskette
|
||||||
|
├── privkey.pem # Private Key
|
||||||
|
├── cert.pem # Zertifikat
|
||||||
|
└── chain.pem # Intermediate Certificate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Zertifikat wird nicht akzeptiert
|
||||||
|
```bash
|
||||||
|
# Prüfe Zertifikats-Gültigkeit
|
||||||
|
openssl x509 -in /etc/yourpart/server.crt -text -noout
|
||||||
|
|
||||||
|
# Prüfe Berechtigungen
|
||||||
|
ls -la /etc/yourpart/server.*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt Challenge fehlgeschlagen
|
||||||
|
```bash
|
||||||
|
# Prüfe Port 80
|
||||||
|
sudo netstat -tlnp | grep :80
|
||||||
|
|
||||||
|
# Prüfe DNS
|
||||||
|
nslookup your-part.de
|
||||||
|
|
||||||
|
# Prüfe Firewall
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Renewal funktioniert nicht
|
||||||
|
```bash
|
||||||
|
# Prüfe Cron Jobs
|
||||||
|
sudo crontab -l
|
||||||
|
|
||||||
|
# Teste Renewal Script
|
||||||
|
sudo /etc/yourpart/renew-ssl.sh
|
||||||
|
|
||||||
|
# Prüfe Logs
|
||||||
|
tail -f /var/log/yourpart/ssl-renewal.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Sicherheit
|
||||||
|
|
||||||
|
### Berechtigungen
|
||||||
|
- **Zertifikat:** `644` (readable by all, writable by owner)
|
||||||
|
- **Private Key:** `600` (readable/writable by owner only)
|
||||||
|
- **Owner:** `yourpart:yourpart`
|
||||||
|
|
||||||
|
### Firewall
|
||||||
|
```bash
|
||||||
|
# Öffne Port 80 für Let's Encrypt Challenge
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
|
||||||
|
# Öffne Port 4551 für WebSocket
|
||||||
|
sudo ufw allow 4551/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Weitere Informationen
|
||||||
|
|
||||||
|
- [Let's Encrypt Dokumentation](https://letsencrypt.org/docs/)
|
||||||
|
- [Certbot Dokumentation](https://certbot.eff.org/docs/)
|
||||||
|
- [libwebsockets SSL](https://libwebsockets.org/lws-api-doc-master/html/group__ssl.html)
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
Bei Problemen:
|
||||||
|
1. Prüfen Sie die Logs: `sudo journalctl -u yourpart-daemon -f`
|
||||||
|
2. Testen Sie die Zertifikate: `openssl s_client -connect your-part.de:4551`
|
||||||
|
3. Prüfen Sie die Firewall: `sudo ufw status`
|
||||||
23
backend/README_TAX.md
Normal file
23
backend/README_TAX.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Falukant Tax Migration & Configuration
|
||||||
|
|
||||||
|
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
||||||
|
|
||||||
|
Migration
|
||||||
|
- A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`.
|
||||||
|
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
||||||
|
|
||||||
|
Runtime configuration
|
||||||
|
- If you want taxes to be forwarded to a treasury account, set environment variable `TREASURY_FALUKANT_USER_ID` to a valid `falukant_user.id`.
|
||||||
|
- If `TREASURY_FALUKANT_USER_ID` is not set, taxes will be calculated and currently not forwarded to any account.
|
||||||
|
|
||||||
|
Implementation notes
|
||||||
|
- Backend service `sellProduct` and `sellAllProducts` now compute tax per-region and credit net to seller and tax to treasury (if configured).
|
||||||
|
- Tax arithmetic uses rounding to 2 decimals. The current implementation performs two separate DB calls (seller, treasury). For strict ledger atomicity consider implementing DB-side booking.
|
||||||
|
|
||||||
|
Cumulative tax behavior
|
||||||
|
- The system now sums `tax_percent` from the sale region and all ancestor regions (recursive up the region tree). This allows defining different tax rates on up to 6 region levels and summing them for final tax percent.
|
||||||
|
- To avoid reducing seller net by taxes, sale prices are inflated by factor = 1 / (1 - cumulativeTax/100). This way the seller receives the original net and the tax is collected separately.
|
||||||
|
|
||||||
|
Testing
|
||||||
|
- After running the migration, test with a small sale and verify `falukant_log.moneyflow` entries for seller and treasury.
|
||||||
|
|
||||||
184
backend/analyze-indexes.js
Executable file
184
backend/analyze-indexes.js
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script zur Analyse und Empfehlung von Indizes
|
||||||
|
*
|
||||||
|
* Analysiert:
|
||||||
|
* - Tabellen mit vielen Sequential Scans
|
||||||
|
* - Fehlende Composite Indizes für häufige JOINs
|
||||||
|
* - Ungenutzte Indizes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Index-Analyse und Empfehlungen\n');
|
||||||
|
console.log('='.repeat(60) + '\n');
|
||||||
|
|
||||||
|
// 1. Tabellen mit vielen Sequential Scans
|
||||||
|
await analyzeSequentialScans();
|
||||||
|
|
||||||
|
// 2. Prüfe häufige JOIN-Patterns
|
||||||
|
await analyzeJoinPatterns();
|
||||||
|
|
||||||
|
// 3. Ungenutzte Indizes
|
||||||
|
await analyzeUnusedIndexes();
|
||||||
|
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('✅ Analyse abgeschlossen\n');
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeSequentialScans() {
|
||||||
|
console.log('📊 1. Tabellen mit vielen Sequential Scans\n');
|
||||||
|
|
||||||
|
const [tables] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
seq_scan,
|
||||||
|
seq_tup_read,
|
||||||
|
idx_scan,
|
||||||
|
seq_tup_read / NULLIF(seq_scan, 0) as avg_rows_per_scan,
|
||||||
|
CASE
|
||||||
|
WHEN seq_scan + idx_scan > 0
|
||||||
|
THEN round((seq_scan::numeric / (seq_scan + idx_scan)) * 100, 2)
|
||||||
|
ELSE 0
|
||||||
|
END as seq_scan_percent
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND seq_scan > 1000
|
||||||
|
ORDER BY seq_tup_read DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (tables.length > 0) {
|
||||||
|
console.log(' ⚠️ Tabellen mit vielen Sequential Scans:');
|
||||||
|
tables.forEach(t => {
|
||||||
|
console.log(`\n ${t.table_name}:`);
|
||||||
|
console.log(` Sequential Scans: ${parseInt(t.seq_scan).toLocaleString()}`);
|
||||||
|
console.log(` Zeilen gelesen: ${parseInt(t.seq_tup_read).toLocaleString()}`);
|
||||||
|
console.log(` Index Scans: ${parseInt(t.idx_scan).toLocaleString()}`);
|
||||||
|
console.log(` Seq Scan Anteil: ${t.seq_scan_percent}%`);
|
||||||
|
console.log(` Ø Zeilen pro Scan: ${parseInt(t.avg_rows_per_scan).toLocaleString()}`);
|
||||||
|
|
||||||
|
if (t.seq_scan_percent > 50) {
|
||||||
|
console.log(` ⚠️ KRITISCH: Mehr als 50% Sequential Scans!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeJoinPatterns() {
|
||||||
|
console.log('🔗 2. Analyse häufiger JOIN-Patterns\n');
|
||||||
|
|
||||||
|
// Prüfe welche Indizes auf knowledge existieren
|
||||||
|
const [knowledgeIndexes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
indexname,
|
||||||
|
indexdef
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'falukant_data'
|
||||||
|
AND tablename = 'knowledge'
|
||||||
|
ORDER BY indexname;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(' Indizes auf falukant_data.knowledge:');
|
||||||
|
if (knowledgeIndexes.length > 0) {
|
||||||
|
knowledgeIndexes.forEach(idx => {
|
||||||
|
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(' Keine Indizes gefunden');
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Empfehlung: Composite Index auf (character_id, product_id)
|
||||||
|
const [knowledgeUsage] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
idx_scan,
|
||||||
|
idx_tup_read,
|
||||||
|
idx_tup_fetch
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname = 'falukant_data'
|
||||||
|
AND relname = 'knowledge'
|
||||||
|
AND indexrelname = 'idx_knowledge_character_id';
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (knowledgeUsage.length > 0) {
|
||||||
|
const usage = knowledgeUsage[0];
|
||||||
|
console.log(' Aktuelle Nutzung von idx_knowledge_character_id:');
|
||||||
|
console.log(` Scans: ${parseInt(usage.idx_scan).toLocaleString()}`);
|
||||||
|
console.log(` Zeilen gelesen: ${parseInt(usage.idx_tup_read).toLocaleString()}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(' 💡 Empfehlung:');
|
||||||
|
console.log(' CREATE INDEX IF NOT EXISTS idx_knowledge_character_product');
|
||||||
|
console.log(' ON falukant_data.knowledge(character_id, product_id);');
|
||||||
|
console.log(' → Wird häufig für JOINs mit character_id UND product_id verwendet\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe character Indizes
|
||||||
|
const [characterIndexes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
indexname,
|
||||||
|
indexdef
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'falukant_data'
|
||||||
|
AND tablename = 'character'
|
||||||
|
ORDER BY indexname;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(' Indizes auf falukant_data.character:');
|
||||||
|
if (characterIndexes.length > 0) {
|
||||||
|
characterIndexes.forEach(idx => {
|
||||||
|
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeUnusedIndexes() {
|
||||||
|
console.log('🗑️ 3. Ungenutzte Indizes\n');
|
||||||
|
|
||||||
|
const [unused] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || indexrelname as index_name,
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||||
|
idx_scan as scans,
|
||||||
|
pg_relation_size(indexrelid) as size_bytes
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND idx_scan = 0
|
||||||
|
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
||||||
|
ORDER BY pg_relation_size(indexrelid) DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (unused.length > 0) {
|
||||||
|
console.log(' ⚠️ Ungenutzte Indizes (> 1MB):');
|
||||||
|
unused.forEach(idx => {
|
||||||
|
console.log(` ${idx.index_name} auf ${idx.table_name}`);
|
||||||
|
console.log(` Größe: ${idx.index_size}, Scans: ${idx.scans}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
console.log(' 💡 Überlege, ob diese Indizes gelöscht werden können:');
|
||||||
|
console.log(' DROP INDEX IF EXISTS <index_name>;');
|
||||||
|
console.log('');
|
||||||
|
} else {
|
||||||
|
console.log(' ✅ Keine großen ungenutzten Indizes gefunden\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import crypto from 'crypto';
|
||||||
import chatRouter from './routers/chatRouter.js';
|
import chatRouter from './routers/chatRouter.js';
|
||||||
import authRouter from './routers/authRouter.js';
|
import authRouter from './routers/authRouter.js';
|
||||||
import navigationRouter from './routers/navigationRouter.js';
|
import navigationRouter from './routers/navigationRouter.js';
|
||||||
@@ -11,11 +12,17 @@ import socialnetworkRouter from './routers/socialnetworkRouter.js';
|
|||||||
import forumRouter from './routers/forumRouter.js';
|
import forumRouter from './routers/forumRouter.js';
|
||||||
import falukantRouter from './routers/falukantRouter.js';
|
import falukantRouter from './routers/falukantRouter.js';
|
||||||
import friendshipRouter from './routers/friendshipRouter.js';
|
import friendshipRouter from './routers/friendshipRouter.js';
|
||||||
|
import modelsProxyRouter from './routers/modelsProxyRouter.js';
|
||||||
import blogRouter from './routers/blogRouter.js';
|
import blogRouter from './routers/blogRouter.js';
|
||||||
import match3Router from './routers/match3Router.js';
|
import match3Router from './routers/match3Router.js';
|
||||||
import taxiRouter from './routers/taxiRouter.js';
|
import taxiRouter from './routers/taxiRouter.js';
|
||||||
import taxiMapRouter from './routers/taxiMapRouter.js';
|
import taxiMapRouter from './routers/taxiMapRouter.js';
|
||||||
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
||||||
|
import termineRouter from './routers/termineRouter.js';
|
||||||
|
import vocabRouter from './routers/vocabRouter.js';
|
||||||
|
import dashboardRouter from './routers/dashboardRouter.js';
|
||||||
|
import newsRouter from './routers/newsRouter.js';
|
||||||
|
import calendarRouter from './routers/calendarRouter.js';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import './jobs/sessionCleanup.js';
|
import './jobs/sessionCleanup.js';
|
||||||
|
|
||||||
@@ -24,16 +31,59 @@ const __dirname = path.dirname(__filename);
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
// Request-Timing (aktivierbar per ENV)
|
||||||
|
// - LOG_SLOW_REQ_MS=200: Logge Requests, die länger dauern als X ms (Default 500)
|
||||||
|
// - LOG_ALL_REQ=1: Logge alle Requests
|
||||||
|
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
|
||||||
|
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
|
||||||
|
const defaultCorsOrigins = [
|
||||||
|
'http://localhost:3000',
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://127.0.0.1:3000',
|
||||||
|
'http://127.0.0.1:5173'
|
||||||
|
];
|
||||||
|
const corsOrigins = (process.env.CORS_ORIGINS || process.env.FRONTEND_URL || '')
|
||||||
|
.split(',')
|
||||||
|
.map((origin) => origin.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const effectiveCorsOrigins = corsOrigins.length > 0 ? corsOrigins : defaultCorsOrigins;
|
||||||
|
const corsAllowAll = process.env.CORS_ALLOW_ALL === '1';
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
|
||||||
|
req.reqId = reqId;
|
||||||
|
res.setHeader('x-request-id', reqId);
|
||||||
|
const t0 = Date.now();
|
||||||
|
res.on('finish', () => {
|
||||||
|
const ms = Date.now() - t0;
|
||||||
|
if (LOG_ALL_REQ || ms >= LOG_SLOW_REQ_MS) {
|
||||||
|
console.log(`⏱️ REQ ${ms}ms ${res.statusCode} ${req.method} ${req.originalUrl} rid=${reqId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
|
origin(origin, callback) {
|
||||||
|
if (!origin) {
|
||||||
|
return callback(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (corsAllowAll || effectiveCorsOrigins.includes(origin)) {
|
||||||
|
return callback(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, false);
|
||||||
|
},
|
||||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'userid', 'authcode', 'userId', 'authCode'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
preflightContinue: false,
|
preflightContinue: false,
|
||||||
optionsSuccessStatus: 204
|
optionsSuccessStatus: 204
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
|
app.options('*', cors(corsOptions));
|
||||||
app.use(express.json()); // To handle JSON request bodies
|
app.use(express.json()); // To handle JSON request bodies
|
||||||
|
|
||||||
app.use('/api/chat', chatRouter);
|
app.use('/api/chat', chatRouter);
|
||||||
@@ -48,13 +98,26 @@ app.use('/api/taxi/highscores', taxiHighscoreRouter);
|
|||||||
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
|
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
|
||||||
app.use('/api/contact', contactRouter);
|
app.use('/api/contact', contactRouter);
|
||||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||||
|
app.use('/api/vocab', vocabRouter);
|
||||||
app.use('/api/forum', forumRouter);
|
app.use('/api/forum', forumRouter);
|
||||||
app.use('/api/falukant', falukantRouter);
|
app.use('/api/falukant', falukantRouter);
|
||||||
app.use('/api/friendships', friendshipRouter);
|
app.use('/api/friendships', friendshipRouter);
|
||||||
|
app.use('/api/models', modelsProxyRouter);
|
||||||
app.use('/api/blog', blogRouter);
|
app.use('/api/blog', blogRouter);
|
||||||
|
app.use('/api/termine', termineRouter);
|
||||||
|
app.use('/api/dashboard', dashboardRouter);
|
||||||
|
app.use('/api/news', newsRouter);
|
||||||
|
app.use('/api/calendar', calendarRouter);
|
||||||
|
|
||||||
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
||||||
|
// /models/* nicht statisch ausliefern – nur über /api/models (Proxy mit Komprimierung)
|
||||||
const frontendDir = path.join(__dirname, '../frontend');
|
const frontendDir = path.join(__dirname, '../frontend');
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/models/')) {
|
||||||
|
return res.status(404).send('Use /api/models/ for 3D models (optimized).');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
app.use(express.static(path.join(frontendDir, 'dist')));
|
app.use(express.static(path.join(frontendDir, 'dist')));
|
||||||
app.get(/^\/(?!api\/).*/, (req, res) => {
|
app.get(/^\/(?!api\/).*/, (req, res) => {
|
||||||
res.sendFile(path.join(frontendDir, 'dist', 'index.html'));
|
res.sendFile(path.join(frontendDir, 'dist', 'index.html'));
|
||||||
|
|||||||
86
backend/check-connections.js
Normal file
86
backend/check-connections.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script zum Prüfen und Bereinigen von PostgreSQL-Verbindungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Prüfe PostgreSQL-Verbindungen...\n');
|
||||||
|
|
||||||
|
// Prüfe aktive Verbindungen
|
||||||
|
const [connections] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
count(*) as total,
|
||||||
|
count(*) FILTER (WHERE state = 'active') as active,
|
||||||
|
count(*) FILTER (WHERE state = 'idle') as idle,
|
||||||
|
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
||||||
|
count(*) FILTER (WHERE usename = current_user) as my_connections
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database();
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📊 Verbindungsstatistik:');
|
||||||
|
console.log(` Gesamt: ${connections[0].total}`);
|
||||||
|
console.log(` Aktiv: ${connections[0].active}`);
|
||||||
|
console.log(` Idle: ${connections[0].idle}`);
|
||||||
|
console.log(` Idle in Transaction: ${connections[0].idle_in_transaction}`);
|
||||||
|
console.log(` Meine Verbindungen: ${connections[0].my_connections}\n`);
|
||||||
|
|
||||||
|
// Prüfe max_connections Limit
|
||||||
|
const [maxConn] = await sequelize.query(`
|
||||||
|
SELECT setting::int as max_connections
|
||||||
|
FROM pg_settings
|
||||||
|
WHERE name = 'max_connections';
|
||||||
|
`);
|
||||||
|
console.log(`📈 Max Connections Limit: ${maxConn[0].max_connections}`);
|
||||||
|
console.log(`📉 Verfügbare Connections: ${maxConn[0].max_connections - connections[0].total}\n`);
|
||||||
|
|
||||||
|
// Zeige alte idle Verbindungen
|
||||||
|
const [oldConnections] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
pid,
|
||||||
|
usename,
|
||||||
|
application_name,
|
||||||
|
state,
|
||||||
|
state_change,
|
||||||
|
now() - state_change as idle_duration,
|
||||||
|
query
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database()
|
||||||
|
AND state = 'idle'
|
||||||
|
AND state_change < now() - interval '1 minute'
|
||||||
|
ORDER BY state_change ASC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (oldConnections.length > 0) {
|
||||||
|
console.log(`⚠️ Gefunden ${oldConnections.length} alte idle Verbindungen (> 1 Minute):`);
|
||||||
|
oldConnections.forEach(conn => {
|
||||||
|
console.log(` PID: ${conn.pid}, User: ${conn.usename}, Idle seit: ${conn.idle_duration}`);
|
||||||
|
});
|
||||||
|
console.log('\n💡 Tipp: Du kannst alte Verbindungen beenden mit:');
|
||||||
|
console.log(' SELECT pg_terminate_backend(pid) FROM pg_stat_activity');
|
||||||
|
console.log(' WHERE datname = current_database() AND state = \'idle\' AND state_change < now() - interval \'5 minutes\';\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob wir nahe am Limit sind
|
||||||
|
const usagePercent = (connections[0].total / maxConn[0].max_connections) * 100;
|
||||||
|
if (usagePercent > 80) {
|
||||||
|
console.log(`⚠️ WARNUNG: ${usagePercent.toFixed(1)}% der verfügbaren Verbindungen werden verwendet!`);
|
||||||
|
console.log(' Es könnte sein, dass nicht genug Verbindungen verfügbar sind.\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
142
backend/check-knowledge-pkey.js
Executable file
142
backend/check-knowledge-pkey.js
Executable file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script zur Analyse des knowledge_pkey Problems
|
||||||
|
*
|
||||||
|
* Prüft warum knowledge_pkey nicht verwendet wird
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Analyse knowledge_pkey Problem\n');
|
||||||
|
console.log('='.repeat(60) + '\n');
|
||||||
|
|
||||||
|
// Prüfe ob knowledge einen Primary Key hat
|
||||||
|
const [pkInfo] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
a.attname as column_name,
|
||||||
|
t.conname as constraint_name,
|
||||||
|
t.contype as constraint_type
|
||||||
|
FROM pg_constraint t
|
||||||
|
JOIN pg_class c ON c.oid = t.conrelid
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(t.conkey)
|
||||||
|
WHERE n.nspname = 'falukant_data'
|
||||||
|
AND c.relname = 'knowledge'
|
||||||
|
AND t.contype = 'p';
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📋 Primary Key Information:');
|
||||||
|
if (pkInfo.length > 0) {
|
||||||
|
pkInfo.forEach(pk => {
|
||||||
|
console.log(` Constraint: ${pk.constraint_name}`);
|
||||||
|
console.log(` Spalte: ${pk.column_name}`);
|
||||||
|
console.log(` Typ: ${pk.constraint_type === 'p' ? 'PRIMARY KEY' : pk.constraint_type}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(' ⚠️ Kein Primary Key gefunden!');
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Prüfe alle Indizes auf knowledge
|
||||||
|
const [allIndexes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
indexname,
|
||||||
|
indexdef,
|
||||||
|
idx_scan,
|
||||||
|
idx_tup_read,
|
||||||
|
idx_tup_fetch
|
||||||
|
FROM pg_indexes
|
||||||
|
LEFT JOIN pg_stat_user_indexes
|
||||||
|
ON pg_stat_user_indexes.indexrelname = pg_indexes.indexname
|
||||||
|
AND pg_stat_user_indexes.schemaname = pg_indexes.schemaname
|
||||||
|
WHERE pg_indexes.schemaname = 'falukant_data'
|
||||||
|
AND pg_indexes.tablename = 'knowledge'
|
||||||
|
ORDER BY indexname;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📊 Alle Indizes auf knowledge:');
|
||||||
|
allIndexes.forEach(idx => {
|
||||||
|
console.log(`\n ${idx.indexname}:`);
|
||||||
|
console.log(` Definition: ${idx.indexdef}`);
|
||||||
|
console.log(` Scans: ${idx.idx_scan ? parseInt(idx.idx_scan).toLocaleString() : 'N/A'}`);
|
||||||
|
console.log(` Zeilen gelesen: ${idx.idx_tup_read ? parseInt(idx.idx_tup_read).toLocaleString() : 'N/A'}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Prüfe Tabellenstruktur
|
||||||
|
const [tableStructure] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable,
|
||||||
|
column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_data'
|
||||||
|
AND table_name = 'knowledge'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📋 Tabellenstruktur:');
|
||||||
|
tableStructure.forEach(col => {
|
||||||
|
console.log(` ${col.column_name}: ${col.data_type} ${col.is_nullable === 'NO' ? 'NOT NULL' : 'NULL'}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Erklärung: Warum knowledge_pkey ungenutzt ist
|
||||||
|
const pkUnused = allIndexes.find(i => i.indexname === 'knowledge_pkey' && (i.idx_scan == null || parseInt(i.idx_scan) === 0));
|
||||||
|
if (pkUnused) {
|
||||||
|
console.log('💡 Warum knowledge_pkey (0 Scans) ungenutzt ist:');
|
||||||
|
console.log(' Alle Zugriffe filtern nach (character_id, product_id), nie nach id.');
|
||||||
|
console.log(' Der PK-Index wird nur für Eindeutigkeit/Referenzen genutzt, nicht für Lookups.');
|
||||||
|
console.log(' idx_knowledge_character_product deckt die tatsächlichen Queries ab.\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Queries mit id (Primary Key) gemacht werden
|
||||||
|
let idUsage = [];
|
||||||
|
try {
|
||||||
|
const [rows] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
query,
|
||||||
|
calls,
|
||||||
|
total_exec_time,
|
||||||
|
mean_exec_time
|
||||||
|
FROM pg_stat_statements
|
||||||
|
WHERE query LIKE '%knowledge%'
|
||||||
|
AND (query LIKE '%knowledge.id%' OR query LIKE '%knowledge%id%')
|
||||||
|
ORDER BY calls DESC
|
||||||
|
LIMIT 5;
|
||||||
|
`);
|
||||||
|
idUsage = rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(' ℹ️ pg_stat_statements nicht verfügbar – keine Query-Statistik.\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idUsage.length > 0) {
|
||||||
|
console.log('🔍 Queries die knowledge.id verwenden:');
|
||||||
|
idUsage.forEach(q => {
|
||||||
|
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}`);
|
||||||
|
console.log(` Query: ${q.query.substring(0, 150)}...`);
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('pg_stat_statements')) {
|
||||||
|
console.log(' ⚠️ pg_stat_statements ist nicht aktiviert oder nicht verfügbar\n');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
55
backend/cleanup-connections.js
Normal file
55
backend/cleanup-connections.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script zum Bereinigen von alten/idle PostgreSQL-Verbindungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🧹 Bereinige alte PostgreSQL-Verbindungen...\n');
|
||||||
|
|
||||||
|
// Beende idle Verbindungen, die älter als 5 Minuten sind (außer unserer eigenen)
|
||||||
|
const [result] = await sequelize.query(`
|
||||||
|
SELECT pg_terminate_backend(pid) as terminated
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database()
|
||||||
|
AND pid <> pg_backend_pid()
|
||||||
|
AND state = 'idle'
|
||||||
|
AND state_change < now() - interval '5 minutes';
|
||||||
|
`);
|
||||||
|
|
||||||
|
const terminated = result.filter(r => r.terminated).length;
|
||||||
|
console.log(`✅ ${terminated} alte idle Verbindungen wurden beendet\n`);
|
||||||
|
|
||||||
|
// Zeige verbleibende Verbindungen
|
||||||
|
const [connections] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
count(*) as total,
|
||||||
|
count(*) FILTER (WHERE state = 'active') as active,
|
||||||
|
count(*) FILTER (WHERE state = 'idle') as idle
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database();
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('📊 Verbleibende Verbindungen:');
|
||||||
|
console.log(` Gesamt: ${connections[0].total}`);
|
||||||
|
console.log(` Aktiv: ${connections[0].active}`);
|
||||||
|
console.log(` Idle: ${connections[0].idle}\n`);
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
if (error.message.includes('SUPERUSER')) {
|
||||||
|
console.error('\n💡 Tipp: Du benötigst Superuser-Rechte oder musst warten, bis Verbindungen freigegeben werden.');
|
||||||
|
console.error(' Versuche es in ein paar Minuten erneut.');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 1235
|
"port": 1236
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,51 @@ const productionEnvPath = '/opt/yourpart/backend/.env';
|
|||||||
const localEnvPath = path.resolve(__dirname, '../.env');
|
const localEnvPath = path.resolve(__dirname, '../.env');
|
||||||
|
|
||||||
let envPath = localEnvPath; // Fallback
|
let envPath = localEnvPath; // Fallback
|
||||||
|
let usingProduction = false;
|
||||||
if (fs.existsSync(productionEnvPath)) {
|
if (fs.existsSync(productionEnvPath)) {
|
||||||
envPath = productionEnvPath;
|
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden
|
||||||
console.log('[env] Lade Produktions-.env:', productionEnvPath);
|
try {
|
||||||
|
fs.accessSync(productionEnvPath, fs.constants.R_OK);
|
||||||
|
envPath = productionEnvPath;
|
||||||
|
usingProduction = true;
|
||||||
|
console.log('[env] Produktions-.env gefunden und lesbar:', productionEnvPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[env] Produktions-.env vorhanden, aber nicht lesbar - verwende lokale .env stattdessen:', productionEnvPath);
|
||||||
|
console.warn('[env] Fehler:', err && err.message);
|
||||||
|
envPath = localEnvPath;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[env] Lade lokale .env:', localEnvPath);
|
console.log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade .env-Datei
|
// Lade .env-Datei (robust gegen Fehler)
|
||||||
console.log('[env] Versuche .env zu laden von:', envPath);
|
console.log('[env] Versuche .env zu laden von:', envPath);
|
||||||
console.log('[env] Datei existiert:', fs.existsSync(envPath));
|
console.log('[env] Datei existiert:', fs.existsSync(envPath));
|
||||||
console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
|
let result;
|
||||||
|
try {
|
||||||
const result = dotenv.config({ path: envPath });
|
result = dotenv.config({ path: envPath });
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.warn('[env] Konnte .env nicht laden:', result.error.message);
|
console.warn('[env] Konnte .env nicht laden:', result.error.message);
|
||||||
console.warn('[env] Fehler-Details:', result.error);
|
console.warn('[env] Fehler-Details:', result.error);
|
||||||
} else {
|
} else {
|
||||||
console.log('[env] .env erfolgreich geladen von:', envPath);
|
console.log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)');
|
||||||
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Sollte nicht passieren, aber falls dotenv intern eine Exception wirft (z.B. EACCES), fange sie ab
|
||||||
|
console.warn('[env] Unerwarteter Fehler beim Laden der .env:', err && err.message);
|
||||||
|
console.warn('[env] Stack:', err && err.stack);
|
||||||
|
if (envPath !== localEnvPath && fs.existsSync(localEnvPath)) {
|
||||||
|
console.log('[env] Versuche stattdessen lokale .env:', localEnvPath);
|
||||||
|
try {
|
||||||
|
result = dotenv.config({ path: localEnvPath });
|
||||||
|
if (!result.error) {
|
||||||
|
console.log('[env] Lokale .env erfolgreich geladen von:', localEnvPath);
|
||||||
|
}
|
||||||
|
} catch (err2) {
|
||||||
|
console.warn('[env] Konnte lokale .env auch nicht laden:', err2 && err2.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: Zeige Redis-Konfiguration
|
// Debug: Zeige Redis-Konfiguration
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class AdminController {
|
|||||||
// User administration
|
// User administration
|
||||||
this.searchUsers = this.searchUsers.bind(this);
|
this.searchUsers = this.searchUsers.bind(this);
|
||||||
this.getUser = this.getUser.bind(this);
|
this.getUser = this.getUser.bind(this);
|
||||||
|
this.getUsers = this.getUsers.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
|
|
||||||
// Rights
|
// Rights
|
||||||
@@ -37,6 +38,14 @@ class AdminController {
|
|||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
this.getUserStatistics = this.getUserStatistics.bind(this);
|
this.getUserStatistics = this.getUserStatistics.bind(this);
|
||||||
|
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
||||||
|
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
||||||
|
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||||
|
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
||||||
|
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
||||||
|
this.createNPCs = this.createNPCs.bind(this);
|
||||||
|
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
||||||
|
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenInterests(req, res) {
|
async getOpenInterests(req, res) {
|
||||||
@@ -74,6 +83,30 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUsers(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
let { ids } = req.query;
|
||||||
|
if (!ids) {
|
||||||
|
return res.status(400).json({ error: 'ids query parameter is required' });
|
||||||
|
}
|
||||||
|
// Unterstütze sowohl Array-Format (ids[]=...) als auch komma-separierten String (ids=...)
|
||||||
|
let hashedIds;
|
||||||
|
if (Array.isArray(ids)) {
|
||||||
|
hashedIds = ids;
|
||||||
|
} else if (typeof ids === 'string') {
|
||||||
|
hashedIds = ids.split(',').map(id => id.trim()).filter(id => id.length > 0);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'ids must be an array or comma-separated string' });
|
||||||
|
}
|
||||||
|
const result = await AdminService.getUsersByHashedIds(requester, hashedIds);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateUser(req, res) {
|
async updateUser(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: requester } = req.headers;
|
const { userid: requester } = req.headers;
|
||||||
@@ -290,6 +323,122 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFalukantRegions(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const regions = await AdminService.getFalukantRegions(userId);
|
||||||
|
res.status(200).json(regions);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFalukantRegionMap(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const { map } = req.body || {};
|
||||||
|
const region = await AdminService.updateFalukantRegionMap(userId, id, map);
|
||||||
|
res.status(200).json(region);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : (error.message === 'regionNotFound' ? 404 : 500);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRegionDistances(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const distances = await AdminService.getRegionDistances(userId);
|
||||||
|
res.status(200).json(distances);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertRegionDistance(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const record = await AdminService.upsertRegionDistance(userId, req.body || {});
|
||||||
|
res.status(200).json(record);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 400;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRegionDistance(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await AdminService.deleteRegionDistance(userId, id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNPCs(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body;
|
||||||
|
const countValue = parseInt(count) || 1;
|
||||||
|
if (countValue < 1 || countValue > 500) {
|
||||||
|
return res.status(400).json({ error: 'Count must be between 1 and 500' });
|
||||||
|
}
|
||||||
|
console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
|
||||||
|
const result = await AdminService.createNPCs(userId, {
|
||||||
|
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
|
||||||
|
minAge: parseInt(minAge) || 0,
|
||||||
|
maxAge: parseInt(maxAge) || 100,
|
||||||
|
minTitleId: parseInt(minTitleId) || 1,
|
||||||
|
maxTitleId: parseInt(maxTitleId) || 19,
|
||||||
|
count: countValue
|
||||||
|
});
|
||||||
|
console.log('[createNPCs] Job created:', result);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[createNPCs] Error:', error);
|
||||||
|
console.error('[createNPCs] Error stack:', error.stack);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTitlesOfNobility(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const titles = await AdminService.getTitlesOfNobility(userId);
|
||||||
|
res.status(200).json(titles);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNPCsCreationStatus(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { jobId } = req.params;
|
||||||
|
const status = await AdminService.getNPCsCreationStatus(userId, jobId);
|
||||||
|
res.status(200).json(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
|
||||||
|
error.message === 'Job not found' ? 404 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getRoomTypes(req, res) {
|
async getRoomTypes(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
|
|||||||
203
backend/controllers/calendarController.js
Normal file
203
backend/controllers/calendarController.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import calendarService from '../services/calendarService.js';
|
||||||
|
|
||||||
|
function getHashedUserId(req) {
|
||||||
|
return req.headers?.userid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/events
|
||||||
|
* Get all events for the authenticated user
|
||||||
|
* Query params: startDate, endDate (optional)
|
||||||
|
*/
|
||||||
|
async getEvents(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
const events = await calendarService.getEvents(hashedUserId, { startDate, endDate });
|
||||||
|
res.json(events);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getEvents:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/events/:id
|
||||||
|
* Get a single event by ID
|
||||||
|
*/
|
||||||
|
async getEvent(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = await calendarService.getEvent(hashedUserId, req.params.id);
|
||||||
|
res.json(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getEvent:', error);
|
||||||
|
if (error.message === 'Event not found') {
|
||||||
|
return res.status(404).json({ error: 'Event not found' });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/calendar/events
|
||||||
|
* Create a new event
|
||||||
|
*/
|
||||||
|
async createEvent(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventData = req.body;
|
||||||
|
if (!eventData.title || !eventData.startDate) {
|
||||||
|
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await calendarService.createEvent(hashedUserId, eventData);
|
||||||
|
res.status(201).json(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar createEvent:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/calendar/events/:id
|
||||||
|
* Update an existing event
|
||||||
|
*/
|
||||||
|
async updateEvent(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventData = req.body;
|
||||||
|
if (!eventData.title || !eventData.startDate) {
|
||||||
|
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await calendarService.updateEvent(hashedUserId, req.params.id, eventData);
|
||||||
|
res.json(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar updateEvent:', error);
|
||||||
|
if (error.message === 'Event not found') {
|
||||||
|
return res.status(404).json({ error: 'Event not found' });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/calendar/events/:id
|
||||||
|
* Delete an event
|
||||||
|
*/
|
||||||
|
async deleteEvent(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await calendarService.deleteEvent(hashedUserId, req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar deleteEvent:', error);
|
||||||
|
if (error.message === 'Event not found') {
|
||||||
|
return res.status(404).json({ error: 'Event not found' });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/birthdays
|
||||||
|
* Get friends' birthdays for a given year
|
||||||
|
* Query params: year (required)
|
||||||
|
*/
|
||||||
|
async getFriendsBirthdays(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
|
const birthdays = await calendarService.getFriendsBirthdays(hashedUserId, year);
|
||||||
|
res.json(birthdays);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getFriendsBirthdays:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/widget/birthdays
|
||||||
|
* Get upcoming birthdays for widget display
|
||||||
|
*/
|
||||||
|
async getWidgetBirthdays(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
const birthdays = await calendarService.getUpcomingBirthdays(hashedUserId, limit);
|
||||||
|
res.json(birthdays);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getWidgetBirthdays:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/widget/upcoming
|
||||||
|
* Get upcoming events for widget display
|
||||||
|
*/
|
||||||
|
async getWidgetUpcoming(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
const events = await calendarService.getUpcomingEvents(hashedUserId, limit);
|
||||||
|
res.json(events);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getWidgetUpcoming:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/widget/mini
|
||||||
|
* Get mini calendar data for widget display
|
||||||
|
*/
|
||||||
|
async getWidgetMiniCalendar(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await calendarService.getMiniCalendarData(hashedUserId);
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calendar getWidgetMiniCalendar:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -13,6 +13,9 @@ class ChatController {
|
|||||||
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
|
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
|
||||||
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
|
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
|
||||||
this.getRoomList = this.getRoomList.bind(this);
|
this.getRoomList = this.getRoomList.bind(this);
|
||||||
|
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
|
||||||
|
this.getOwnRooms = this.getOwnRooms.bind(this);
|
||||||
|
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMessages(req, res) {
|
async getMessages(req, res) {
|
||||||
@@ -175,6 +178,41 @@ class ChatController {
|
|||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRoomCreateOptions(req, res) {
|
||||||
|
try {
|
||||||
|
const options = await chatService.getRoomCreateOptions();
|
||||||
|
res.status(200).json(options);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwnRooms(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const rooms = await chatService.getOwnRooms(hashedUserId);
|
||||||
|
res.status(200).json(rooms);
|
||||||
|
} catch (error) {
|
||||||
|
const status = error.message === 'user_not_found' ? 404 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOwnRoom(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: hashedUserId } = req.headers;
|
||||||
|
const roomId = Number.parseInt(req.params.id, 10);
|
||||||
|
if (!Number.isInteger(roomId) || roomId <= 0) {
|
||||||
|
return res.status(400).json({ error: 'invalid_room_id' });
|
||||||
|
}
|
||||||
|
await chatService.deleteOwnRoom(hashedUserId, roomId);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatController;
|
export default ChatController;
|
||||||
|
|||||||
50
backend/controllers/dashboardController.js
Normal file
50
backend/controllers/dashboardController.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import dashboardService from '../services/dashboardService.js';
|
||||||
|
|
||||||
|
function getHashedUserId(req) {
|
||||||
|
return req.headers?.userid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/** Liste der möglichen Widget-Typen (öffentlich, keine Auth nötig wenn gewünscht – aktuell mit Auth). */
|
||||||
|
async getAvailableWidgets(req, res) {
|
||||||
|
try {
|
||||||
|
const list = await dashboardService.getAvailableWidgets();
|
||||||
|
res.json(list);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dashboard getAvailableWidgets:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getConfig(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const config = await dashboardService.getConfig(hashedUserId);
|
||||||
|
res.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dashboard getConfig:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setConfig(req, res) {
|
||||||
|
const hashedUserId = getHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
const config = req.body;
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
return res.status(400).json({ error: 'Invalid config' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await dashboardService.setConfig(hashedUserId, config);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dashboard setConfig:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -26,10 +26,13 @@ class FalukantController {
|
|||||||
}, { successStatus: 201 });
|
}, { successStatus: 201 });
|
||||||
|
|
||||||
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
|
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
|
||||||
|
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
|
||||||
|
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
||||||
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
||||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||||
|
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, productId, quantity } = req.body;
|
const { branchId, productId, quantity } = req.body;
|
||||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||||
@@ -55,6 +58,10 @@ class FalukantController {
|
|||||||
if (!page) page = 1;
|
if (!page) page = 1;
|
||||||
return this.service.moneyHistory(userId, page, filter);
|
return this.service.moneyHistory(userId, page, filter);
|
||||||
});
|
});
|
||||||
|
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
|
||||||
|
const { range } = req.body || {};
|
||||||
|
return this.service.moneyHistoryGraph(userId, range || '24h');
|
||||||
|
});
|
||||||
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
|
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
|
||||||
this.buyStorage = this._wrapWithUser((userId, req) => {
|
this.buyStorage = this._wrapWithUser((userId, req) => {
|
||||||
const { branchId, amount, stockTypeId } = req.body;
|
const { branchId, amount, stockTypeId } = req.body;
|
||||||
@@ -91,15 +98,40 @@ class FalukantController {
|
|||||||
if (!result) throw { status: 404, message: 'No family data found' };
|
if (!result) throw { status: 404, message: 'No family data found' };
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||||
|
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
|
||||||
|
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||||
|
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
||||||
|
try {
|
||||||
|
return await this.service.cancelWooing(userId);
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.name === 'PreconditionError' && e.message === 'cancelTooSoon') {
|
||||||
|
throw { status: 412, message: 'cancelTooSoon', retryAt: e.meta?.retryAt };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, { successStatus: 202 });
|
||||||
this.getGifts = this._wrapWithUser((userId) => {
|
this.getGifts = this._wrapWithUser((userId) => {
|
||||||
console.log('🔍 getGifts called with userId:', userId);
|
console.log('🔍 getGifts called with userId:', userId);
|
||||||
return this.service.getGifts(userId);
|
return this.service.getGifts(userId);
|
||||||
});
|
});
|
||||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||||
this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
|
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
||||||
|
try {
|
||||||
|
return await this.service.sendGift(userId, req.body.giftId);
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
|
||||||
|
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
||||||
|
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
||||||
|
this.executeReputationAction = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
|
||||||
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
||||||
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
||||||
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
||||||
@@ -119,6 +151,17 @@ class FalukantController {
|
|||||||
const { characterId: childId, firstName } = req.body;
|
const { characterId: childId, firstName } = req.body;
|
||||||
return this.service.baptise(userId, childId, firstName);
|
return this.service.baptise(userId, childId, firstName);
|
||||||
});
|
});
|
||||||
|
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
|
||||||
|
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
|
||||||
|
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
|
||||||
|
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
|
||||||
|
const { officeTypeId, regionId } = req.body;
|
||||||
|
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
|
||||||
|
});
|
||||||
|
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
||||||
|
const { applicationId, decision } = req.body;
|
||||||
|
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
||||||
|
});
|
||||||
|
|
||||||
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
||||||
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||||
@@ -134,7 +177,16 @@ class FalukantController {
|
|||||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||||
|
|
||||||
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
||||||
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
|
this.healthActivity = this._wrapWithUser(async (userId, req) => {
|
||||||
|
try {
|
||||||
|
return await this.service.healthActivity(userId, req.body.measureTr);
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
|
||||||
|
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||||
@@ -143,6 +195,41 @@ class FalukantController {
|
|||||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||||
|
|
||||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||||
|
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||||
|
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
|
||||||
|
const productId = parseInt(req.query.productId, 10);
|
||||||
|
const regionId = parseInt(req.query.regionId, 10);
|
||||||
|
if (Number.isNaN(productId) || Number.isNaN(regionId)) {
|
||||||
|
throw new Error('productId and regionId are required');
|
||||||
|
}
|
||||||
|
return this.service.getProductPriceInRegion(userId, productId, regionId);
|
||||||
|
});
|
||||||
|
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
|
||||||
|
const regionId = parseInt(req.query.regionId, 10);
|
||||||
|
if (Number.isNaN(regionId)) {
|
||||||
|
throw new Error('regionId is required');
|
||||||
|
}
|
||||||
|
return this.service.getAllProductPricesInRegion(userId, regionId);
|
||||||
|
});
|
||||||
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
|
const productId = parseInt(req.query.productId, 10);
|
||||||
|
const currentPrice = parseFloat(req.query.currentPrice);
|
||||||
|
const currentRegionId = req.query.currentRegionId ? parseInt(req.query.currentRegionId, 10) : null;
|
||||||
|
if (Number.isNaN(productId) || Number.isNaN(currentPrice)) {
|
||||||
|
throw new Error('productId and currentPrice are required');
|
||||||
|
}
|
||||||
|
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
||||||
|
});
|
||||||
|
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
|
||||||
|
const body = req.body || {};
|
||||||
|
const items = Array.isArray(body.items) ? body.items : [];
|
||||||
|
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
|
||||||
|
const valid = items.map(i => ({
|
||||||
|
productId: parseInt(i.productId, 10),
|
||||||
|
currentPrice: parseFloat(i.currentPrice)
|
||||||
|
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
||||||
|
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
|
||||||
|
});
|
||||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||||
|
|
||||||
@@ -150,6 +237,7 @@ class FalukantController {
|
|||||||
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
||||||
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
||||||
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
||||||
|
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
||||||
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
|
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
|
||||||
|
|
||||||
this.searchUsers = this._wrapWithUser((userId, req) => {
|
this.searchUsers = this._wrapWithUser((userId, req) => {
|
||||||
@@ -181,6 +269,33 @@ class FalukantController {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||||
|
this.buyVehicles = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||||
|
{ successStatus: 201 }
|
||||||
|
);
|
||||||
|
this.getVehicles = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||||
|
);
|
||||||
|
this.createTransport = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.createTransport(userId, req.body),
|
||||||
|
{ successStatus: 201 }
|
||||||
|
);
|
||||||
|
this.getTransportRoute = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||||
|
);
|
||||||
|
this.getBranchTransports = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.getBranchTransports(userId, req.params.branchId)
|
||||||
|
);
|
||||||
|
this.repairVehicle = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||||
|
{ successStatus: 200 }
|
||||||
|
);
|
||||||
|
this.repairAllVehicles = this._wrapWithUser(
|
||||||
|
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||||
|
{ successStatus: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -197,7 +312,13 @@ class FalukantController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Controller error:', error);
|
console.error('Controller error:', error);
|
||||||
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
||||||
res.status(status).json({ error: error.message || 'Internal error' });
|
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
|
||||||
|
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
|
||||||
|
const { status: errorStatus, ...errorData } = error;
|
||||||
|
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
|
||||||
|
} else {
|
||||||
|
res.status(status).json({ error: error.message || 'Internal error' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import UserRight from '../models/community/user_right.js';
|
|||||||
import UserRightType from '../models/type/user_right.js';
|
import UserRightType from '../models/type/user_right.js';
|
||||||
import UserParamType from '../models/type/user_param.js';
|
import UserParamType from '../models/type/user_param.js';
|
||||||
import FalukantUser from '../models/falukant/data/user.js';
|
import FalukantUser from '../models/falukant/data/user.js';
|
||||||
|
import VocabService from '../services/vocabService.js';
|
||||||
|
|
||||||
const menuStructure = {
|
const menuStructure = {
|
||||||
home: {
|
home: {
|
||||||
@@ -117,10 +118,6 @@ const menuStructure = {
|
|||||||
visible: ["hasfalukantaccount"],
|
visible: ["hasfalukantaccount"],
|
||||||
path: "/falukant/branch"
|
path: "/falukant/branch"
|
||||||
},
|
},
|
||||||
directors: {
|
|
||||||
visible: ["hasfalukantaccount"],
|
|
||||||
path: "/falukant/directors"
|
|
||||||
},
|
|
||||||
family: {
|
family: {
|
||||||
visible: ["hasfalukantaccount"],
|
visible: ["hasfalukantaccount"],
|
||||||
path: "/falukant/family"
|
path: "/falukant/family"
|
||||||
@@ -181,6 +178,30 @@ const menuStructure = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
personal: {
|
||||||
|
visible: ["all"],
|
||||||
|
icon: "profile16.png",
|
||||||
|
children: {
|
||||||
|
sprachenlernen: {
|
||||||
|
visible: ["all"],
|
||||||
|
children: {
|
||||||
|
vocabtrainer: {
|
||||||
|
visible: ["all"],
|
||||||
|
path: "/socialnetwork/vocab",
|
||||||
|
showVocabLanguages: 1 // Flag für dynamische Sprachen-Liste
|
||||||
|
},
|
||||||
|
sprachkurse: {
|
||||||
|
visible: ["all"],
|
||||||
|
path: "/socialnetwork/vocab/courses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
visible: ["all"],
|
||||||
|
path: "/personal/calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
icon: "settings16.png",
|
icon: "settings16.png",
|
||||||
@@ -251,6 +272,10 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "chatrooms"],
|
visible: ["mainadmin", "chatrooms"],
|
||||||
path: "/admin/chatrooms"
|
path: "/admin/chatrooms"
|
||||||
},
|
},
|
||||||
|
servicesStatus: {
|
||||||
|
visible: ["mainadmin"],
|
||||||
|
path: "/admin/services/status"
|
||||||
|
},
|
||||||
interests: {
|
interests: {
|
||||||
visible: ["mainadmin", "interests"],
|
visible: ["mainadmin", "interests"],
|
||||||
path: "/admin/interests"
|
path: "/admin/interests"
|
||||||
@@ -270,6 +295,14 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant"],
|
||||||
path: "/admin/falukant/database"
|
path: "/admin/falukant/database"
|
||||||
},
|
},
|
||||||
|
mapEditor: {
|
||||||
|
visible: ["mainadmin", "falukant"],
|
||||||
|
path: "/admin/falukant/map"
|
||||||
|
},
|
||||||
|
createNPC: {
|
||||||
|
visible: ["mainadmin", "falukant"],
|
||||||
|
path: "/admin/falukant/create-npc"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
minigames: {
|
minigames: {
|
||||||
@@ -292,6 +325,7 @@ const menuStructure = {
|
|||||||
class NavigationController {
|
class NavigationController {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.menu = this.menu.bind(this);
|
this.menu = this.menu.bind(this);
|
||||||
|
this.vocabService = new VocabService();
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateAge(birthDate) {
|
calculateAge(birthDate) {
|
||||||
@@ -361,6 +395,11 @@ class NavigationController {
|
|||||||
const age = this.calculateAge(birthDate);
|
const age = this.calculateAge(birthDate);
|
||||||
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
||||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
||||||
|
|
||||||
|
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
|
||||||
|
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt
|
||||||
|
// Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an
|
||||||
|
|
||||||
res.status(200).json(filteredMenu);
|
res.status(200).json(filteredMenu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching menu:', error);
|
console.error('Error fetching menu:', error);
|
||||||
|
|||||||
21
backend/controllers/newsController.js
Normal file
21
backend/controllers/newsController.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import newsService from '../services/newsService.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/news?counter=0&language=de&category=top
|
||||||
|
* counter = wievieltes News-Widget aufgerufen wird (0, 1, 2, …), damit keine doppelten Artikel.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
async getNews(req, res) {
|
||||||
|
const counter = Math.max(0, parseInt(req.query.counter, 10) || 0);
|
||||||
|
const language = (req.query.language || 'de').slice(0, 10);
|
||||||
|
const category = (req.query.category || 'top').slice(0, 50);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { results, nextPage } = await newsService.getNews({ counter, language, category });
|
||||||
|
res.json({ results, nextPage });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('News getNews:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'News konnten nicht geladen werden.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
43
backend/controllers/termineController.js
Normal file
43
backend/controllers/termineController.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
class TermineController {
|
||||||
|
async getTermine(req, res) {
|
||||||
|
try {
|
||||||
|
const csvPath = path.join(__dirname, '../data/termine.csv');
|
||||||
|
const csvContent = fs.readFileSync(csvPath, 'utf-8');
|
||||||
|
|
||||||
|
const lines = csvContent.trim().split('\n');
|
||||||
|
const headers = lines[0].split(',');
|
||||||
|
|
||||||
|
const termine = lines.slice(1).map(line => {
|
||||||
|
const values = line.split(',');
|
||||||
|
const termin = {};
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
termin[header] = values[index] || '';
|
||||||
|
});
|
||||||
|
return termin;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sortiere nach Datum
|
||||||
|
termine.sort((a, b) => new Date(a.datum) - new Date(b.datum));
|
||||||
|
|
||||||
|
// Filtere nur zukünftige Termine
|
||||||
|
const heute = new Date();
|
||||||
|
heute.setHours(0, 0, 0, 0);
|
||||||
|
const zukuenftigeTermine = termine.filter(t => new Date(t.datum) >= heute);
|
||||||
|
|
||||||
|
res.status(200).json(zukuenftigeTermine);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading termine.csv:', error);
|
||||||
|
res.status(500).json({ error: 'Could not load termine' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TermineController();
|
||||||
|
|
||||||
80
backend/controllers/vocabController.js
Normal file
80
backend/controllers/vocabController.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import VocabService from '../services/vocabService.js';
|
||||||
|
|
||||||
|
function extractHashedUserId(req) {
|
||||||
|
return req.headers?.userid;
|
||||||
|
}
|
||||||
|
|
||||||
|
class VocabController {
|
||||||
|
constructor() {
|
||||||
|
this.service = new VocabService();
|
||||||
|
|
||||||
|
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
|
||||||
|
this.listAllLanguages = this._wrapWithUser(() => this.service.listAllLanguages());
|
||||||
|
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
|
||||||
|
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
|
||||||
|
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
|
||||||
|
|
||||||
|
this.listChapters = this._wrapWithUser((userId, req) => this.service.listChapters(userId, req.params.languageId));
|
||||||
|
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
||||||
|
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
||||||
|
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
||||||
|
|
||||||
|
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
|
||||||
|
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
|
||||||
|
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
|
||||||
|
|
||||||
|
// Courses
|
||||||
|
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
|
||||||
|
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
|
||||||
|
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
|
||||||
|
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
|
||||||
|
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
|
||||||
|
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
|
||||||
|
|
||||||
|
// Lessons
|
||||||
|
this.getLesson = this._wrapWithUser((userId, req) => this.service.getLesson(userId, req.params.lessonId));
|
||||||
|
this.addLessonToCourse = this._wrapWithUser((userId, req) => this.service.addLessonToCourse(userId, req.params.courseId, req.body), { successStatus: 201 });
|
||||||
|
this.updateLesson = this._wrapWithUser((userId, req) => this.service.updateLesson(userId, req.params.lessonId, req.body));
|
||||||
|
this.deleteLesson = this._wrapWithUser((userId, req) => this.service.deleteLesson(userId, req.params.lessonId));
|
||||||
|
|
||||||
|
// Enrollment
|
||||||
|
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
|
||||||
|
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
|
||||||
|
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
|
||||||
|
|
||||||
|
// Progress
|
||||||
|
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
|
||||||
|
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
|
||||||
|
|
||||||
|
// Grammar Exercises
|
||||||
|
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
|
||||||
|
this.createGrammarExercise = this._wrapWithUser((userId, req) => this.service.createGrammarExercise(userId, req.params.lessonId, req.body), { successStatus: 201 });
|
||||||
|
this.getGrammarExercisesForLesson = this._wrapWithUser((userId, req) => this.service.getGrammarExercisesForLesson(userId, req.params.lessonId));
|
||||||
|
this.getGrammarExercise = this._wrapWithUser((userId, req) => this.service.getGrammarExercise(userId, req.params.exerciseId));
|
||||||
|
this.checkGrammarExerciseAnswer = this._wrapWithUser((userId, req) => this.service.checkGrammarExerciseAnswer(userId, req.params.exerciseId, req.body.answer));
|
||||||
|
this.getGrammarExerciseProgress = this._wrapWithUser((userId, req) => this.service.getGrammarExerciseProgress(userId, req.params.lessonId));
|
||||||
|
this.updateGrammarExercise = this._wrapWithUser((userId, req) => this.service.updateGrammarExercise(userId, req.params.exerciseId, req.body));
|
||||||
|
this.deleteGrammarExercise = this._wrapWithUser((userId, req) => this.service.deleteGrammarExercise(userId, req.params.exerciseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapWithUser(fn, { successStatus = 200 } = {}) {
|
||||||
|
return async (req, res) => {
|
||||||
|
try {
|
||||||
|
const hashedUserId = extractHashedUserId(req);
|
||||||
|
if (!hashedUserId) {
|
||||||
|
return res.status(400).json({ error: 'Missing user identifier' });
|
||||||
|
}
|
||||||
|
const result = await fn(hashedUserId, req, res);
|
||||||
|
res.status(successStatus).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Controller error:', error);
|
||||||
|
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
||||||
|
res.status(status).json({ error: error.message || 'Internal error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VocabController;
|
||||||
|
|
||||||
|
|
||||||
159
backend/create-performance-indexes.js
Executable file
159
backend/create-performance-indexes.js
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script zum Erstellen von Performance-Indizes
|
||||||
|
*
|
||||||
|
* Erstellt Indizes basierend auf der Analyse häufiger Queries:
|
||||||
|
* - inventory: stock_id
|
||||||
|
* - stock: branch_id
|
||||||
|
* - production: branch_id
|
||||||
|
* - director: employer_user_id
|
||||||
|
* - knowledge: (character_id, product_id) composite
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Erstelle Performance-Indizes\n');
|
||||||
|
console.log('='.repeat(60) + '\n');
|
||||||
|
|
||||||
|
const indexes = [
|
||||||
|
{
|
||||||
|
name: 'idx_knowledge_character_product',
|
||||||
|
table: 'falukant_data.knowledge',
|
||||||
|
columns: '(character_id, product_id)',
|
||||||
|
description: 'Composite Index für JOINs mit character_id UND product_id',
|
||||||
|
critical: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_inventory_stock_id',
|
||||||
|
table: 'falukant_data.inventory',
|
||||||
|
columns: '(stock_id)',
|
||||||
|
description: 'Index für WHERE inventory.stock_id = ...',
|
||||||
|
critical: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_stock_branch_id',
|
||||||
|
table: 'falukant_data.stock',
|
||||||
|
columns: '(branch_id)',
|
||||||
|
description: 'Index für WHERE stock.branch_id = ...',
|
||||||
|
critical: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_production_branch_id',
|
||||||
|
table: 'falukant_data.production',
|
||||||
|
columns: '(branch_id)',
|
||||||
|
description: 'Index für WHERE production.branch_id = ...',
|
||||||
|
critical: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_director_employer_user_id',
|
||||||
|
table: 'falukant_data.director',
|
||||||
|
columns: '(employer_user_id)',
|
||||||
|
description: 'Index für WHERE director.employer_user_id = ...',
|
||||||
|
critical: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_production_start_timestamp',
|
||||||
|
table: 'falukant_data.production',
|
||||||
|
columns: '(start_timestamp)',
|
||||||
|
description: 'Index für WHERE production.start_timestamp <= ...',
|
||||||
|
critical: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'idx_director_last_salary_payout',
|
||||||
|
table: 'falukant_data.director',
|
||||||
|
columns: '(last_salary_payout)',
|
||||||
|
description: 'Index für WHERE director.last_salary_payout < ...',
|
||||||
|
critical: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`📋 ${indexes.length} Indizes werden erstellt:\n`);
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let errors = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < indexes.length; i++) {
|
||||||
|
const idx = indexes[i];
|
||||||
|
const criticalMark = idx.critical ? ' ⚠️ KRITISCH' : '';
|
||||||
|
|
||||||
|
console.log(`[${i + 1}/${indexes.length}] ${idx.name}${criticalMark}`);
|
||||||
|
console.log(` Tabelle: ${idx.table}`);
|
||||||
|
console.log(` Spalten: ${idx.columns}`);
|
||||||
|
console.log(` Beschreibung: ${idx.description}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Index bereits existiert
|
||||||
|
const [existing] = await sequelize.query(`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE schemaname || '.' || tablename = '${idx.table}'
|
||||||
|
AND indexname = '${idx.name}'
|
||||||
|
) as exists;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (existing[0].exists) {
|
||||||
|
console.log(` ⏭️ Index existiert bereits, überspringe\n`);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle Index
|
||||||
|
const startTime = Date.now();
|
||||||
|
await sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS ${idx.name}
|
||||||
|
ON ${idx.table} USING btree ${idx.columns};
|
||||||
|
`);
|
||||||
|
|
||||||
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
console.log(` ✅ Erstellt in ${duration}s\n`);
|
||||||
|
created++;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ Fehler: ${error.message}\n`);
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log(`✅ Zusammenfassung:`);
|
||||||
|
console.log(` Erstellt: ${created}`);
|
||||||
|
console.log(` Übersprungen: ${skipped}`);
|
||||||
|
console.log(` Fehler: ${errors}\n`);
|
||||||
|
|
||||||
|
// ANALYZE ausführen, damit PostgreSQL die neuen Indizes berücksichtigt
|
||||||
|
const tablesToAnalyze = [
|
||||||
|
'falukant_data.knowledge',
|
||||||
|
'falukant_data.inventory',
|
||||||
|
'falukant_data.stock',
|
||||||
|
'falukant_data.production',
|
||||||
|
'falukant_data.director'
|
||||||
|
];
|
||||||
|
if (created > 0) {
|
||||||
|
console.log('📊 Führe ANALYZE auf betroffenen Tabellen aus...\n');
|
||||||
|
for (const table of tablesToAnalyze) {
|
||||||
|
try {
|
||||||
|
await sequelize.query(`ANALYZE ${table};`);
|
||||||
|
console.log(` ✅ ANALYZE ${table};`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` ⚠️ ${table}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
123
backend/daemonServer.js
Normal file
123
backend/daemonServer.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import WebSocket, { WebSocketServer } from 'ws';
|
||||||
|
import https from 'https';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const PORT = Number.parseInt(process.env.DAEMON_PORT || '4551', 10);
|
||||||
|
const USE_TLS = process.env.DAEMON_TLS === '1';
|
||||||
|
const TLS_KEY_PATH = process.env.DAEMON_TLS_KEY_PATH;
|
||||||
|
const TLS_CERT_PATH = process.env.DAEMON_TLS_CERT_PATH;
|
||||||
|
const TLS_CA_PATH = process.env.DAEMON_TLS_CA_PATH; // optional
|
||||||
|
|
||||||
|
// Einfache In-Memory-Struktur für Verbindungen (für spätere Erweiterungen)
|
||||||
|
const connections = new Set();
|
||||||
|
|
||||||
|
function createServer() {
|
||||||
|
let wss;
|
||||||
|
|
||||||
|
if (USE_TLS) {
|
||||||
|
if (!TLS_KEY_PATH || !TLS_CERT_PATH) {
|
||||||
|
console.error('[Daemon] DAEMON_TLS=1 gesetzt, aber DAEMON_TLS_KEY_PATH/DAEMON_TLS_CERT_PATH fehlen.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const httpsServer = https.createServer({
|
||||||
|
key: fs.readFileSync(TLS_KEY_PATH),
|
||||||
|
cert: fs.readFileSync(TLS_CERT_PATH),
|
||||||
|
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
||||||
|
});
|
||||||
|
wss = new WebSocketServer({ server: httpsServer });
|
||||||
|
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
||||||
|
httpsServer.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
||||||
|
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
|
||||||
|
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.on('connection', (ws, req) => {
|
||||||
|
const peer = req.socket.remoteAddress + ':' + req.socket.remotePort;
|
||||||
|
ws.isAlive = true;
|
||||||
|
ws.userId = null;
|
||||||
|
connections.add(ws);
|
||||||
|
|
||||||
|
console.log(`[Daemon] Neue Verbindung von ${peer}`);
|
||||||
|
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
try {
|
||||||
|
if (message.toString() === 'pong') {
|
||||||
|
// Client-Pong für unser Ping
|
||||||
|
ws.isAlive = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(message.toString());
|
||||||
|
|
||||||
|
// Vom Frontend gesendet nach Verbindungsaufbau
|
||||||
|
if (data.event === 'setUserId' && data.data?.userId) {
|
||||||
|
ws.userId = data.data.userId;
|
||||||
|
console.log(`[Daemon] setUserId erhalten: ${ws.userId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin-Dialog: WebSocket-Log anfordern
|
||||||
|
if (data.event === 'getWebsocketLog') {
|
||||||
|
const response = {
|
||||||
|
event: 'getWebsocketLogResponse',
|
||||||
|
entries: [] // aktuell keine Log-Historie implementiert
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platzhalter für spätere Events
|
||||||
|
// console.log('[Daemon] Unbekanntes Event:', data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Daemon] Fehler beim Verarbeiten einer Nachricht:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
connections.delete(ws);
|
||||||
|
console.log('[Daemon] Verbindung geschlossen');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
console.error('[Daemon] WebSocket-Fehler (Verbindung):', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Einfache Ping/Pong-Mechanik, damit Verbindungen sauber erkannt werden
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
for (const ws of connections) {
|
||||||
|
if (ws.isAlive === false) {
|
||||||
|
console.log('[Daemon] Verbindung wegen fehlendem Pong beendet');
|
||||||
|
ws.terminate();
|
||||||
|
connections.delete(ws);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ws.isAlive = false;
|
||||||
|
try {
|
||||||
|
ws.send('ping');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Daemon] Fehler beim Senden von Ping:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
wss.on('close', () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
connections.clear();
|
||||||
|
console.log('[Daemon] Server gestoppt');
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on('error', (err) => {
|
||||||
|
console.error('[Daemon] Server-Fehler:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wss;
|
||||||
|
}
|
||||||
|
|
||||||
|
createServer();
|
||||||
|
|
||||||
|
|
||||||
7
backend/data/termine.csv
Normal file
7
backend/data/termine.csv
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
datum,titel,beschreibung,ort,uhrzeit
|
||||||
|
2025-10-07,Vereinsmeisterschaften 2025 Doppel,Die Vereinsmeisterschaften 2025 im Doppel finden im Rahmen des Erwachsenentrainings statt.,,,
|
||||||
|
2026-01-17,Vereinsmeisterschaften 2025 Einzel,Die Vereinsmeisterschaften 2025 im Einzel finden in der Schulturnhalle statt. Bitte vormerken!,,10:00
|
||||||
|
2025-12-18,Weihnachtsfeier 2025,Die Weihnachtsfeier 2025 findet im Gasthaus „Zum Einhorn" in FFM-Bonames statt. Beginn 19:00 Uhr (bitte vormerken),Gasthaus „Zum Einhorn" FFM-Bonames,19:00
|
||||||
|
2025-09-14,VR-Cup,Zwei VR-Cups am 14.09.2025 (jeweils 12 und 16 Uhr),,12:00 und 16:00
|
||||||
|
2025-10-19,VR-Cup,Zwei VR-Cups am 19.10.2025 (jeweils 12 und 16 Uhr),,12:00 und 16:00
|
||||||
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 4 and column 91.
|
479
backend/diagnose-db-performance.js
Executable file
479
backend/diagnose-db-performance.js
Executable file
@@ -0,0 +1,479 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umfassendes Diagnose-Script für Datenbank-Performance
|
||||||
|
*
|
||||||
|
* Untersucht:
|
||||||
|
* - Verbindungsstatistiken
|
||||||
|
* - Langsame Queries
|
||||||
|
* - Tabellengrößen und Bloat
|
||||||
|
* - Indizes (fehlende/ungenutzte)
|
||||||
|
* - Vacuum/Analyze Status
|
||||||
|
* - Locking/Blocking
|
||||||
|
* - Query-Statistiken
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './config/loadEnv.js';
|
||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Datenbank-Performance-Diagnose\n');
|
||||||
|
console.log('='.repeat(60) + '\n');
|
||||||
|
|
||||||
|
// 1. Verbindungsstatistiken
|
||||||
|
await checkConnections();
|
||||||
|
|
||||||
|
// 2. Langsame Queries (wenn pg_stat_statements aktiviert ist)
|
||||||
|
await checkSlowQueries();
|
||||||
|
|
||||||
|
// 3. Tabellengrößen und Bloat
|
||||||
|
await checkTableSizes();
|
||||||
|
|
||||||
|
// 4. Indizes prüfen
|
||||||
|
await checkIndexes();
|
||||||
|
|
||||||
|
// 5. Vacuum/Analyze Status
|
||||||
|
await checkVacuumStatus();
|
||||||
|
|
||||||
|
// 6. Locking/Blocking
|
||||||
|
await checkLocks();
|
||||||
|
|
||||||
|
// 7. Query-Statistiken (wenn pg_stat_statements aktiviert ist)
|
||||||
|
await checkQueryStats();
|
||||||
|
|
||||||
|
// 8. Connection Pool Status
|
||||||
|
await checkConnectionPool();
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
console.log('✅ Diagnose abgeschlossen\n');
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkConnections() {
|
||||||
|
console.log('📊 1. Verbindungsstatistiken\n');
|
||||||
|
|
||||||
|
const [connections] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
count(*) as total,
|
||||||
|
count(*) FILTER (WHERE state = 'active') as active,
|
||||||
|
count(*) FILTER (WHERE state = 'idle') as idle,
|
||||||
|
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
||||||
|
count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database();
|
||||||
|
`);
|
||||||
|
|
||||||
|
const conn = connections[0];
|
||||||
|
console.log(` Gesamt: ${conn.total}`);
|
||||||
|
console.log(` Aktiv: ${conn.active}`);
|
||||||
|
console.log(` Idle: ${conn.idle}`);
|
||||||
|
console.log(` Idle in Transaction: ${conn.idle_in_transaction}`);
|
||||||
|
console.log(` Wartend: ${conn.waiting}\n`);
|
||||||
|
|
||||||
|
const [maxConn] = await sequelize.query(`
|
||||||
|
SELECT setting::int as max_connections
|
||||||
|
FROM pg_settings
|
||||||
|
WHERE name = 'max_connections';
|
||||||
|
`);
|
||||||
|
|
||||||
|
const usagePercent = (conn.total / maxConn[0].max_connections) * 100;
|
||||||
|
console.log(` Max Connections: ${maxConn[0].max_connections}`);
|
||||||
|
console.log(` Auslastung: ${usagePercent.toFixed(1)}%\n`);
|
||||||
|
|
||||||
|
if (usagePercent > 80) {
|
||||||
|
console.log(' ⚠️ WARNUNG: Hohe Verbindungsauslastung!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige lange laufende Queries
|
||||||
|
const [longRunning] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
pid,
|
||||||
|
usename,
|
||||||
|
application_name,
|
||||||
|
state,
|
||||||
|
now() - query_start as duration,
|
||||||
|
wait_event_type,
|
||||||
|
wait_event,
|
||||||
|
left(query, 100) as query_preview
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database()
|
||||||
|
AND state != 'idle'
|
||||||
|
AND now() - query_start > interval '5 seconds'
|
||||||
|
ORDER BY query_start ASC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (longRunning.length > 0) {
|
||||||
|
console.log(' ⚠️ Lange laufende Queries (> 5 Sekunden):');
|
||||||
|
longRunning.forEach(q => {
|
||||||
|
const duration = Math.round(q.duration.total_seconds);
|
||||||
|
console.log(` PID ${q.pid}: ${duration}s - ${q.query_preview}...`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSlowQueries() {
|
||||||
|
console.log('🐌 2. Langsame Queries (pg_stat_statements)\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob pg_stat_statements aktiviert ist
|
||||||
|
const [extension] = await sequelize.query(`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
||||||
|
) as exists;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!extension[0].exists) {
|
||||||
|
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.');
|
||||||
|
console.log(' 💡 Aktivieren mit: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [slowQueries] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
left(query, 100) as query_preview,
|
||||||
|
calls,
|
||||||
|
total_exec_time,
|
||||||
|
mean_exec_time,
|
||||||
|
max_exec_time,
|
||||||
|
(total_exec_time / sum(total_exec_time) OVER ()) * 100 as percent_total
|
||||||
|
FROM pg_stat_statements
|
||||||
|
WHERE mean_exec_time > 100 -- Queries mit > 100ms Durchschnitt
|
||||||
|
ORDER BY total_exec_time DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (slowQueries.length > 0) {
|
||||||
|
console.log(' Top 10 langsamste Queries (nach Gesamtzeit):');
|
||||||
|
slowQueries.forEach((q, i) => {
|
||||||
|
console.log(` ${i + 1}. ${q.query_preview}...`);
|
||||||
|
console.log(` Aufrufe: ${q.calls}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms, Max: ${q.max_exec_time.toFixed(2)}ms`);
|
||||||
|
console.log(` Gesamtzeit: ${q.total_exec_time.toFixed(2)}ms (${q.percent_total.toFixed(1)}%)\n`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(' ✅ Keine sehr langsamen Queries gefunden (> 100ms Durchschnitt)\n');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ⚠️ Fehler beim Abrufen der Query-Statistiken: ${error.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTableSizes() {
|
||||||
|
console.log('📦 3. Tabellengrößen und Bloat\n');
|
||||||
|
|
||||||
|
const [tableSizes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || relname as full_table_name,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size,
|
||||||
|
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as table_size,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) as indexes_size,
|
||||||
|
n_live_tup as row_count,
|
||||||
|
n_dead_tup as dead_rows,
|
||||||
|
CASE
|
||||||
|
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
||||||
|
ELSE 0
|
||||||
|
END as dead_row_percent,
|
||||||
|
last_vacuum,
|
||||||
|
last_autovacuum,
|
||||||
|
last_analyze,
|
||||||
|
last_autoanalyze
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC
|
||||||
|
LIMIT 20;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (tableSizes.length > 0) {
|
||||||
|
console.log(' Top 20 größte Tabellen:');
|
||||||
|
tableSizes.forEach((t, i) => {
|
||||||
|
console.log(` ${i + 1}. ${t.full_table_name}`);
|
||||||
|
console.log(` Größe: ${t.total_size} (Tabelle: ${t.table_size}, Indizes: ${t.indexes_size})`);
|
||||||
|
console.log(` Zeilen: ${parseInt(t.row_count).toLocaleString()}, Tote Zeilen: ${parseInt(t.dead_rows).toLocaleString()} (${t.dead_row_percent}%)`);
|
||||||
|
|
||||||
|
if (parseFloat(t.dead_row_percent) > 20) {
|
||||||
|
console.log(` ⚠️ Hoher Bloat-Anteil! Vacuum empfohlen.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.last_vacuum || t.last_autovacuum) {
|
||||||
|
const lastVacuum = t.last_vacuum || t.last_autovacuum;
|
||||||
|
const daysSinceVacuum = Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24));
|
||||||
|
if (daysSinceVacuum > 7) {
|
||||||
|
console.log(` ⚠️ Letztes Vacuum: ${daysSinceVacuum} Tage her`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIndexes() {
|
||||||
|
console.log('🔍 4. Indizes-Analyse\n');
|
||||||
|
|
||||||
|
// Fehlende Indizes (basierend auf pg_stat_user_tables)
|
||||||
|
const [missingIndexes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
seq_scan,
|
||||||
|
seq_tup_read,
|
||||||
|
idx_scan,
|
||||||
|
seq_tup_read / NULLIF(seq_scan, 0) as avg_seq_read
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND seq_scan > 1000
|
||||||
|
AND seq_tup_read / NULLIF(seq_scan, 0) > 1000
|
||||||
|
ORDER BY seq_tup_read DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (missingIndexes.length > 0) {
|
||||||
|
console.log(' ⚠️ Tabellen mit vielen Sequential Scans (möglicherweise fehlende Indizes):');
|
||||||
|
missingIndexes.forEach(t => {
|
||||||
|
console.log(` ${t.table_name}: ${t.seq_scan} seq scans, ${parseInt(t.seq_tup_read).toLocaleString()} Zeilen gelesen`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ungenutzte Indizes
|
||||||
|
const [unusedIndexes] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || indexrelname as index_name,
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||||
|
idx_scan as scans
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND idx_scan = 0
|
||||||
|
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
||||||
|
ORDER BY pg_relation_size(indexrelid) DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (unusedIndexes.length > 0) {
|
||||||
|
console.log(' ⚠️ Ungenutzte Indizes (> 1MB, nie verwendet):');
|
||||||
|
unusedIndexes.forEach(idx => {
|
||||||
|
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (0 Scans)`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Bloat
|
||||||
|
const [indexBloat] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || indexrelname as index_name,
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||||
|
idx_scan as scans,
|
||||||
|
idx_tup_read as tuples_read,
|
||||||
|
idx_tup_fetch as tuples_fetched
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND pg_relation_size(indexrelid) > 10 * 1024 * 1024 -- Größer als 10MB
|
||||||
|
ORDER BY pg_relation_size(indexrelid) DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (indexBloat.length > 0) {
|
||||||
|
console.log(' Top 10 größte Indizes:');
|
||||||
|
indexBloat.forEach(idx => {
|
||||||
|
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (${idx.scans} Scans)`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkVacuumStatus() {
|
||||||
|
console.log('🧹 5. Vacuum/Analyze Status\n');
|
||||||
|
|
||||||
|
const [vacuumStats] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
schemaname || '.' || relname as table_name,
|
||||||
|
last_vacuum,
|
||||||
|
last_autovacuum,
|
||||||
|
last_analyze,
|
||||||
|
last_autoanalyze,
|
||||||
|
n_dead_tup,
|
||||||
|
n_live_tup,
|
||||||
|
CASE
|
||||||
|
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
||||||
|
ELSE 0
|
||||||
|
END as dead_percent
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||||
|
AND (
|
||||||
|
(last_vacuum IS NULL AND last_autovacuum IS NULL)
|
||||||
|
OR (last_vacuum < now() - interval '7 days' AND last_autovacuum < now() - interval '7 days')
|
||||||
|
OR n_dead_tup > 10000
|
||||||
|
)
|
||||||
|
ORDER BY n_dead_tup DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (vacuumStats.length > 0) {
|
||||||
|
console.log(' ⚠️ Tabellen, die Vacuum benötigen könnten:');
|
||||||
|
vacuumStats.forEach(t => {
|
||||||
|
const lastVacuum = t.last_vacuum || t.last_autovacuum || 'Nie';
|
||||||
|
const daysSince = lastVacuum !== 'Nie'
|
||||||
|
? Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24))
|
||||||
|
: '∞';
|
||||||
|
console.log(` ${t.table_name}:`);
|
||||||
|
console.log(` Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
|
||||||
|
console.log(` Letztes Vacuum: ${lastVacuum} (${daysSince} Tage)`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
} else {
|
||||||
|
console.log(' ✅ Alle Tabellen sind aktuell gevacuumt\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLocks() {
|
||||||
|
console.log('🔒 6. Locking/Blocking\n');
|
||||||
|
|
||||||
|
const [locks] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
blocked_locks.pid AS blocked_pid,
|
||||||
|
blocked_activity.usename AS blocked_user,
|
||||||
|
blocking_locks.pid AS blocking_pid,
|
||||||
|
blocking_activity.usename AS blocking_user,
|
||||||
|
blocked_activity.query AS blocked_statement,
|
||||||
|
blocking_activity.query AS blocking_statement,
|
||||||
|
blocked_activity.application_name AS blocked_app,
|
||||||
|
blocking_activity.application_name AS blocking_app
|
||||||
|
FROM pg_catalog.pg_locks blocked_locks
|
||||||
|
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
||||||
|
JOIN pg_catalog.pg_locks blocking_locks
|
||||||
|
ON blocking_locks.locktype = blocked_locks.locktype
|
||||||
|
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
|
||||||
|
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
||||||
|
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
|
||||||
|
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
|
||||||
|
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
|
||||||
|
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
|
||||||
|
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
|
||||||
|
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
|
||||||
|
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
|
||||||
|
AND blocking_locks.pid != blocked_locks.pid
|
||||||
|
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
||||||
|
WHERE NOT blocked_locks.granted;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (locks.length > 0) {
|
||||||
|
console.log(' ⚠️ Blockierte Queries gefunden:');
|
||||||
|
locks.forEach(lock => {
|
||||||
|
console.log(` Blockiert: PID ${lock.blocked_pid} (${lock.blocked_user})`);
|
||||||
|
console.log(` Blockiert von: PID ${lock.blocking_pid} (${lock.blocking_user})`);
|
||||||
|
console.log(` Blockierte Query: ${lock.blocked_statement.substring(0, 100)}...`);
|
||||||
|
console.log(` Blockierende Query: ${lock.blocking_statement.substring(0, 100)}...\n`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(' ✅ Keine blockierten Queries gefunden\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige alle aktiven Locks
|
||||||
|
const [allLocks] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
locktype,
|
||||||
|
relation::regclass as relation,
|
||||||
|
mode,
|
||||||
|
granted,
|
||||||
|
pid
|
||||||
|
FROM pg_locks
|
||||||
|
WHERE relation IS NOT NULL
|
||||||
|
AND NOT granted
|
||||||
|
LIMIT 10;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (allLocks.length > 0) {
|
||||||
|
console.log(' ⚠️ Wartende Locks:');
|
||||||
|
allLocks.forEach(lock => {
|
||||||
|
console.log(` ${lock.locktype} auf ${lock.relation}: ${lock.mode} (PID ${lock.pid})`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkQueryStats() {
|
||||||
|
console.log('📈 7. Query-Statistiken\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [extension] = await sequelize.query(`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
||||||
|
) as exists;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!extension[0].exists) {
|
||||||
|
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [topQueries] = await sequelize.query(`
|
||||||
|
SELECT
|
||||||
|
left(query, 80) as query_preview,
|
||||||
|
calls,
|
||||||
|
total_exec_time,
|
||||||
|
mean_exec_time,
|
||||||
|
(100 * total_exec_time / sum(total_exec_time) OVER ()) as percent_total
|
||||||
|
FROM pg_stat_statements
|
||||||
|
WHERE query NOT LIKE '%pg_stat_statements%'
|
||||||
|
ORDER BY calls DESC
|
||||||
|
LIMIT 5;
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (topQueries.length > 0) {
|
||||||
|
console.log(' Top 5 häufigste Queries:');
|
||||||
|
topQueries.forEach((q, i) => {
|
||||||
|
console.log(` ${i + 1}. ${q.query_preview}...`);
|
||||||
|
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ⚠️ Fehler: ${error.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkConnectionPool() {
|
||||||
|
console.log('🏊 8. Connection Pool Status\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole Pool-Konfiguration aus Sequelize Config
|
||||||
|
const config = sequelize.config;
|
||||||
|
const poolConfig = config.pool || {};
|
||||||
|
|
||||||
|
console.log(` Pool-Konfiguration:`);
|
||||||
|
console.log(` Max: ${poolConfig.max || 'N/A'}`);
|
||||||
|
console.log(` Min: ${poolConfig.min || 'N/A'}`);
|
||||||
|
console.log(` Acquire Timeout: ${poolConfig.acquire || 'N/A'}ms`);
|
||||||
|
console.log(` Idle Timeout: ${poolConfig.idle || 'N/A'}ms`);
|
||||||
|
console.log(` Evict Interval: ${poolConfig.evict || 'N/A'}ms\n`);
|
||||||
|
|
||||||
|
// Versuche Pool-Status zu bekommen
|
||||||
|
const pool = sequelize.connectionManager.pool;
|
||||||
|
if (pool) {
|
||||||
|
const poolSize = pool.size || 0;
|
||||||
|
const poolUsed = pool.used || 0;
|
||||||
|
const poolPending = pool.pending || 0;
|
||||||
|
|
||||||
|
console.log(` Pool-Status:`);
|
||||||
|
console.log(` Größe: ${poolSize}`);
|
||||||
|
console.log(` Verwendet: ${poolUsed}`);
|
||||||
|
console.log(` Wartend: ${poolPending}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(` ℹ️ Pool-Objekt nicht verfügbar\n`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ⚠️ Fehler beim Abrufen der Pool-Informationen: ${error.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
34
backend/fix-pgcrypto-extension.js
Normal file
34
backend/fix-pgcrypto-extension.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { sequelize } from './utils/sequelize.js';
|
||||||
|
|
||||||
|
async function fixPgCryptoExtension() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Aktiviere pgcrypto Erweiterung...');
|
||||||
|
|
||||||
|
await sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
|
||||||
|
|
||||||
|
console.log('✅ pgcrypto Erweiterung erfolgreich aktiviert');
|
||||||
|
|
||||||
|
// Prüfe ob die Erweiterung aktiviert ist
|
||||||
|
const result = await sequelize.query(`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto'
|
||||||
|
) as extension_exists;
|
||||||
|
`, { type: sequelize.QueryTypes.SELECT });
|
||||||
|
|
||||||
|
if (result[0]?.extension_exists) {
|
||||||
|
console.log('✅ Bestätigung: pgcrypto Erweiterung ist aktiviert');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Warnung: pgcrypto Erweiterung konnte nicht aktiviert werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Aktivieren der pgcrypto Erweiterung:', error.message);
|
||||||
|
console.error('Stack:', error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixPgCryptoExtension();
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
{
|
||||||
|
tableName: 'falukant_user',
|
||||||
|
schema: 'falukant_data'
|
||||||
|
},
|
||||||
|
'last_nobility_advance_at',
|
||||||
|
{
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn(
|
||||||
|
{
|
||||||
|
tableName: 'falukant_user',
|
||||||
|
schema: 'falukant_data'
|
||||||
|
},
|
||||||
|
'last_nobility_advance_at'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// 1) Add character_name column to notification table
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
ADD COLUMN IF NOT EXISTS character_name text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 1b) Add character_id column so triggers and application can set a reference
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
ADD COLUMN IF NOT EXISTS character_id integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create an index on character_id to speed lookups (if not exists)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_notification_character_id ON falukant_log.notification (character_id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2) Create helper function to populate character_name from character_id or user_id
|
||||||
|
// - Resolve name via character_id if present
|
||||||
|
// - Fallback to a character for the same user_id when character_id is NULL
|
||||||
|
// - Only set NEW.character_name when the column exists and is NULL
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
|
||||||
|
RETURNS TRIGGER AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_first_name TEXT;
|
||||||
|
v_last_name TEXT;
|
||||||
|
v_char_id INTEGER;
|
||||||
|
v_column_exists BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
-- check if target column exists in the notification table
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_log' AND table_name = 'notification' AND column_name = 'character_name'
|
||||||
|
) INTO v_column_exists;
|
||||||
|
|
||||||
|
IF NOT v_column_exists THEN
|
||||||
|
-- Nothing to do when target column absent
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- only populate when column is NULL
|
||||||
|
IF NEW.character_name IS NOT NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- prefer explicit character_id
|
||||||
|
v_char_id := NEW.character_id;
|
||||||
|
|
||||||
|
-- when character_id is null, try to find a character for the user_id
|
||||||
|
IF v_char_id IS NULL AND NEW.user_id IS NOT NULL THEN
|
||||||
|
-- choose a representative character: the one with highest id for this user (change if different policy required)
|
||||||
|
SELECT id INTO v_char_id
|
||||||
|
FROM falukant_data.character
|
||||||
|
WHERE user_id = NEW.user_id
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_char_id IS NOT NULL THEN
|
||||||
|
SELECT pf.name, pl.name
|
||||||
|
INTO v_first_name, v_last_name
|
||||||
|
FROM falukant_data.character c
|
||||||
|
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
|
||||||
|
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
|
||||||
|
WHERE c.id = v_char_id;
|
||||||
|
|
||||||
|
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
|
||||||
|
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
|
||||||
|
ELSE
|
||||||
|
NEW.character_name := ('#' || v_char_id::text);
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- last resort fallback: use user_id as identifier if present
|
||||||
|
IF NEW.user_id IS NOT NULL THEN
|
||||||
|
NEW.character_name := ('#u' || NEW.user_id::text);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$function$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3) Create trigger that runs before insert to populate the column
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
|
||||||
|
CREATE TRIGGER trg_populate_notification_character_name
|
||||||
|
BEFORE INSERT ON falukant_log.notification
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION falukant_log.populate_notification_character_name();
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name();
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
-- drop index if exists
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX falukant_log.idx_notification_character_id';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
DROP COLUMN IF EXISTS character_name;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_log.notification
|
||||||
|
DROP COLUMN IF EXISTS character_id;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Add nullable weather_type_id column
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
ADD COLUMN IF NOT EXISTS weather_type_id integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add foreign key constraint if not exists
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.constraint_schema = tc.constraint_schema
|
||||||
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
AND tc.constraint_schema = 'falukant_data'
|
||||||
|
AND tc.table_name = 'production'
|
||||||
|
AND kcu.column_name = 'weather_type_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data.production
|
||||||
|
ADD CONSTRAINT fk_production_weather_type
|
||||||
|
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// create index to speed lookups
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_production_weather_type_id ON falukant_data.production (weather_type_id);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_production_weather_type;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
|
||||||
|
) THEN
|
||||||
|
EXECUTE 'DROP INDEX falukant_data.idx_production_weather_type_id';
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.production
|
||||||
|
DROP COLUMN IF EXISTS weather_type_id;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.stock
|
||||||
|
ADD COLUMN IF NOT EXISTS product_quality integer;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.stock
|
||||||
|
DROP COLUMN IF EXISTS product_quality;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// falukant_data.character.reputation (integer, default random 20..80)
|
||||||
|
// Wichtig: Schema explizit angeben
|
||||||
|
// Vorgehen:
|
||||||
|
// - Spalte anlegen (falls noch nicht vorhanden)
|
||||||
|
// - bestehende Zeilen initialisieren (random 20..80)
|
||||||
|
// - DEFAULT setzen (random 20..80)
|
||||||
|
// - NOT NULL + CHECK 0..100 erzwingen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_data'
|
||||||
|
AND table_name = 'character'
|
||||||
|
AND column_name = 'reputation'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ADD COLUMN reputation integer;
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Backfill: nur NULLs initialisieren (damit bestehende Werte nicht überschrieben werden)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_data."character"
|
||||||
|
SET reputation = (floor(random()*61)+20)::int
|
||||||
|
WHERE reputation IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// DEFAULT + NOT NULL (nach Backfill)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ALTER COLUMN reputation SET DEFAULT (floor(random()*61)+20)::int;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ALTER COLUMN reputation SET NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Enforce 0..100 at DB level (percent)
|
||||||
|
// (IF NOT EXISTS pattern, because deployments can be re-run)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_class t ON t.oid = c.conrelid
|
||||||
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||||
|
WHERE c.conname = 'character_reputation_0_100_chk'
|
||||||
|
AND n.nspname = 'falukant_data'
|
||||||
|
AND t.relname = 'character'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ADD CONSTRAINT character_reputation_0_100_chk
|
||||||
|
CHECK (reputation >= 0 AND reputation <= 100);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
DROP CONSTRAINT IF EXISTS character_reputation_0_100_chk;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
DROP COLUMN IF EXISTS reputation;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
47
backend/migrations/20251220001000-add-reputation-actions.cjs
Normal file
47
backend/migrations/20251220001000-add-reputation-actions.cjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Typ-Tabelle (konfigurierbar ohne Code): falukant_type.reputation_action
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS falukant_type.reputation_action (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
tr text NOT NULL UNIQUE,
|
||||||
|
cost integer NOT NULL CHECK (cost >= 0),
|
||||||
|
base_gain integer NOT NULL CHECK (base_gain >= 0),
|
||||||
|
decay_factor double precision NOT NULL CHECK (decay_factor > 0 AND decay_factor <= 1),
|
||||||
|
min_gain integer NOT NULL DEFAULT 0 CHECK (min_gain >= 0),
|
||||||
|
decay_window_days integer NOT NULL DEFAULT 7 CHECK (decay_window_days >= 1 AND decay_window_days <= 365)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Log-Tabelle: falukant_log.reputation_action
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS falukant_log.reputation_action (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
falukant_user_id integer NOT NULL,
|
||||||
|
action_type_id integer NOT NULL,
|
||||||
|
cost integer NOT NULL CHECK (cost >= 0),
|
||||||
|
base_gain integer NOT NULL CHECK (base_gain >= 0),
|
||||||
|
gain integer NOT NULL CHECK (gain >= 0),
|
||||||
|
times_used_before integer NOT NULL CHECK (times_used_before >= 0),
|
||||||
|
action_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS reputation_action_log_user_type_idx
|
||||||
|
ON falukant_log.reputation_action (falukant_user_id, action_type_id);
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS reputation_action_log_ts_idx
|
||||||
|
ON falukant_log.reputation_action (action_timestamp);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_log.reputation_action;`);
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_type.reputation_action;`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Für bereits existierende Installationen: Spalte sicherstellen + Backfill
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
ADD COLUMN IF NOT EXISTS decay_window_days integer;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_type.reputation_action
|
||||||
|
SET decay_window_days = 7
|
||||||
|
WHERE decay_window_days IS NULL;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
ALTER COLUMN decay_window_days SET DEFAULT 7;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
ALTER COLUMN decay_window_days SET NOT NULL;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
ADD CONSTRAINT reputation_action_decay_window_days_chk
|
||||||
|
CHECK (decay_window_days >= 1 AND decay_window_days <= 365);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// optional: wieder entfernen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.reputation_action
|
||||||
|
DROP COLUMN IF EXISTS decay_window_days;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Idempotentes Seed: legt Ruf-Aktionen an bzw. aktualisiert sie anhand "tr"
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO falukant_type.reputation_action
|
||||||
|
(tr, cost, base_gain, decay_factor, min_gain, decay_window_days)
|
||||||
|
VALUES
|
||||||
|
('soup_kitchen', 500, 2, 0.85, 0, 7),
|
||||||
|
('library_donation', 5000, 4, 0.88, 0, 7),
|
||||||
|
('well_build', 8000, 4, 0.87, 0, 7),
|
||||||
|
('scholarships', 10000, 5, 0.87, 0, 7),
|
||||||
|
('church_hospice', 12000, 5, 0.87, 0, 7),
|
||||||
|
('school_funding', 15000, 6, 0.88, 0, 7),
|
||||||
|
('orphanage_build', 20000, 7, 0.90, 0, 7),
|
||||||
|
('bridge_build', 25000, 7, 0.90, 0, 7),
|
||||||
|
('hospital_donation', 30000, 8, 0.90, 0, 7),
|
||||||
|
('patronage', 40000, 9, 0.91, 0, 7),
|
||||||
|
('statue_build', 50000, 10, 0.92, 0, 7)
|
||||||
|
ON CONFLICT (tr) DO UPDATE SET
|
||||||
|
cost = EXCLUDED.cost,
|
||||||
|
base_gain = EXCLUDED.base_gain,
|
||||||
|
decay_factor = EXCLUDED.decay_factor,
|
||||||
|
min_gain = EXCLUDED.min_gain,
|
||||||
|
decay_window_days = EXCLUDED.decay_window_days;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// Entfernt nur die gesetzten Seeds (tr-basiert)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DELETE FROM falukant_type.reputation_action
|
||||||
|
WHERE tr IN (
|
||||||
|
'soup_kitchen',
|
||||||
|
'library_donation',
|
||||||
|
'well_build',
|
||||||
|
'scholarships',
|
||||||
|
'church_hospice',
|
||||||
|
'school_funding',
|
||||||
|
'orphanage_build',
|
||||||
|
'bridge_build',
|
||||||
|
'hospital_donation',
|
||||||
|
'patronage',
|
||||||
|
'statue_build'
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Ensure column exists
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ADD COLUMN IF NOT EXISTS condition integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Backfill nulls (legacy data)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_data.vehicle
|
||||||
|
SET condition = 100
|
||||||
|
WHERE condition IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Clamp out-of-range values defensively
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_data.vehicle
|
||||||
|
SET condition = GREATEST(0, LEAST(100, condition))
|
||||||
|
WHERE condition < 0 OR condition > 100;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Default + NOT NULL
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ALTER COLUMN condition SET DEFAULT 100;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ALTER COLUMN condition SET NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Check constraint 0..100
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ADD CONSTRAINT vehicle_condition_0_100_chk
|
||||||
|
CHECK (condition >= 0 AND condition <= 100);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// Keep the column, but remove constraint/default to be reversible
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ALTER COLUMN condition DROP DEFAULT;
|
||||||
|
`);
|
||||||
|
// NOT NULL not reverted to avoid introducing NULLs on rollback; can be adjusted if needed
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.director
|
||||||
|
ADD COLUMN IF NOT EXISTS may_repair_vehicles boolean;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_data.director
|
||||||
|
SET may_repair_vehicles = true
|
||||||
|
WHERE may_repair_vehicles IS NULL;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.director
|
||||||
|
ALTER COLUMN may_repair_vehicles SET DEFAULT true;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.director
|
||||||
|
ALTER COLUMN may_repair_vehicles SET NOT NULL;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// optional rollback: drop column
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.director
|
||||||
|
DROP COLUMN IF EXISTS may_repair_vehicles;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// Sprache / Set, das geteilt werden kann
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_language (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
owner_user_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
share_code TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_language_owner_fk
|
||||||
|
FOREIGN KEY (owner_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Abos (Freunde)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_language_subscription (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
language_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_language_subscription_user_fk
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_language_subscription_language_fk
|
||||||
|
FOREIGN KEY (language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_language_owner_idx
|
||||||
|
ON community.vocab_language(owner_user_id);
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx
|
||||||
|
ON community.vocab_language_subscription(user_id);
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx
|
||||||
|
ON community.vocab_language_subscription(language_id);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language_subscription;`);
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language;`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// Kapitel innerhalb einer Sprache
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_chapter (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
language_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
created_by_user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_chapter_language_fk
|
||||||
|
FOREIGN KEY (language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_chapter_creator_fk
|
||||||
|
FOREIGN KEY (created_by_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx
|
||||||
|
ON community.vocab_chapter(language_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Lexeme/Wörter (wir deduplizieren pro Sprache über normalized)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_lexeme (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
language_id INTEGER NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
normalized TEXT NOT NULL,
|
||||||
|
created_by_user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_lexeme_language_fk
|
||||||
|
FOREIGN KEY (language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_lexeme_creator_fk
|
||||||
|
FOREIGN KEY (created_by_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx
|
||||||
|
ON community.vocab_lexeme(language_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// n:m Zuordnung pro Kapitel: Lernwort ↔ Referenzwort (Mehrdeutigkeiten möglich)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
chapter_id INTEGER NOT NULL,
|
||||||
|
learning_lexeme_id INTEGER NOT NULL,
|
||||||
|
reference_lexeme_id INTEGER NOT NULL,
|
||||||
|
created_by_user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_chlex_chapter_fk
|
||||||
|
FOREIGN KEY (chapter_id)
|
||||||
|
REFERENCES community.vocab_chapter(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_chlex_learning_fk
|
||||||
|
FOREIGN KEY (learning_lexeme_id)
|
||||||
|
REFERENCES community.vocab_lexeme(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_chlex_reference_fk
|
||||||
|
FOREIGN KEY (reference_lexeme_id)
|
||||||
|
REFERENCES community.vocab_lexeme(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_chlex_creator_fk
|
||||||
|
FOREIGN KEY (created_by_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx
|
||||||
|
ON community.vocab_chapter_lexeme(chapter_id);
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx
|
||||||
|
ON community.vocab_chapter_lexeme(learning_lexeme_id);
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
|
||||||
|
ON community.vocab_chapter_lexeme(reference_lexeme_id);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter_lexeme;`);
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_lexeme;`);
|
||||||
|
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter;`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.region
|
||||||
|
ADD COLUMN IF NOT EXISTS tax_percent numeric NOT NULL DEFAULT 7;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_data.region
|
||||||
|
DROP COLUMN IF EXISTS tax_percent;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// 1) add backup column for original sell_cost (idempotent)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_type.product
|
||||||
|
ADD COLUMN IF NOT EXISTS original_sell_cost numeric;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2) if original_sell_cost is not set, copy current sell_cost into it
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_type.product
|
||||||
|
SET original_sell_cost = sell_cost
|
||||||
|
WHERE original_sell_cost IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3) compute max cumulative tax across regions and increase sell_cost accordingly
|
||||||
|
// We use the maximum cumulative tax (worst-case) so sellers are neutral across regions.
|
||||||
|
// Formula: neutral_sell = CEIL(original_sell_cost * (1 / (1 - max_total/100)))
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
|
||||||
|
UNION ALL
|
||||||
|
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
|
||||||
|
FROM falukant_data.region r
|
||||||
|
JOIN ancestors a ON r.id = a.parent_id
|
||||||
|
), totals AS (
|
||||||
|
SELECT start_id, COALESCE(SUM(tax_percent), 0) AS total FROM ancestors GROUP BY start_id
|
||||||
|
), mm AS (
|
||||||
|
SELECT COALESCE(MAX(total),0) AS max_total FROM totals
|
||||||
|
)
|
||||||
|
UPDATE falukant_type.product
|
||||||
|
SET sell_cost = CEIL(original_sell_cost * (CASE WHEN (1 - mm.max_total/100) <= 0 THEN 1 ELSE (1 / (1 - mm.max_total/100)) END))
|
||||||
|
FROM mm
|
||||||
|
WHERE original_sell_cost IS NOT NULL;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_type.product
|
||||||
|
DROP COLUMN IF EXISTS sell_cost_min_neutral;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE IF EXISTS falukant_type.product
|
||||||
|
DROP COLUMN IF EXISTS sell_cost_max_neutral;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Rollback: Remove indexes for director proposals and character queries
|
||||||
|
-- Created: 2026-01-12
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_character_region_user_created;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_character_region_user;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_character_user_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_director_proposal_employer_character;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_director_character_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_director_employer_user_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_knowledge_character_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_relationship_character1_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_child_relation_father_id;
|
||||||
|
DROP INDEX IF EXISTS falukant_data.idx_child_relation_mother_id;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- Migration: Add indexes for director proposals and character queries
|
||||||
|
-- Created: 2026-01-12
|
||||||
|
|
||||||
|
-- Index für schnelle Suche nach NPCs in einer Region (mit Altersbeschränkung)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_character_region_user_created
|
||||||
|
ON falukant_data.character (region_id, user_id, created_at)
|
||||||
|
WHERE user_id IS NULL;
|
||||||
|
|
||||||
|
-- Index für schnelle Suche nach NPCs ohne Altersbeschränkung
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_character_region_user
|
||||||
|
ON falukant_data.character (region_id, user_id)
|
||||||
|
WHERE user_id IS NULL;
|
||||||
|
|
||||||
|
-- Index für Character-Suche nach user_id (wichtig für getFamily, getDirectorForBranch)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_character_user_id
|
||||||
|
ON falukant_data.character (user_id);
|
||||||
|
|
||||||
|
-- Index für Director-Proposals
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_director_proposal_employer_character
|
||||||
|
ON falukant_data.director_proposal (employer_user_id, director_character_id);
|
||||||
|
|
||||||
|
-- Index für aktive Direktoren
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_director_character_id
|
||||||
|
ON falukant_data.director (director_character_id);
|
||||||
|
|
||||||
|
-- Index für Director-Suche nach employer_user_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_director_employer_user_id
|
||||||
|
ON falukant_data.director (employer_user_id);
|
||||||
|
|
||||||
|
-- Index für Knowledge-Berechnung
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_knowledge_character_id
|
||||||
|
ON falukant_data.knowledge (character_id);
|
||||||
|
|
||||||
|
-- Index für Relationships (getFamily)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_relationship_character1_id
|
||||||
|
ON falukant_data.relationship (character1_id);
|
||||||
|
|
||||||
|
-- Index für ChildRelations (getFamily)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_child_relation_father_id
|
||||||
|
ON falukant_data.child_relation (father_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_child_relation_mother_id
|
||||||
|
ON falukant_data.child_relation (mother_id);
|
||||||
132
backend/migrations/20260115000000-add-vocab-courses.cjs
Normal file
132
backend/migrations/20260115000000-add-vocab-courses.cjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// Kurs-Tabelle
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_course (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
owner_user_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
language_id INTEGER NOT NULL,
|
||||||
|
difficulty_level INTEGER DEFAULT 1,
|
||||||
|
is_public BOOLEAN DEFAULT false,
|
||||||
|
share_code TEXT,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_course_owner_fk
|
||||||
|
FOREIGN KEY (owner_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_language_fk
|
||||||
|
FOREIGN KEY (language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Lektionen innerhalb eines Kurses
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
course_id INTEGER NOT NULL,
|
||||||
|
chapter_id INTEGER NOT NULL,
|
||||||
|
lesson_number INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_course_lesson_course_fk
|
||||||
|
FOREIGN KEY (course_id)
|
||||||
|
REFERENCES community.vocab_course(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_lesson_chapter_fk
|
||||||
|
FOREIGN KEY (chapter_id)
|
||||||
|
REFERENCES community.vocab_chapter(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Einschreibungen in Kurse
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
course_id INTEGER NOT NULL,
|
||||||
|
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_course_enrollment_user_fk
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_enrollment_course_fk
|
||||||
|
FOREIGN KEY (course_id)
|
||||||
|
REFERENCES community.vocab_course(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Fortschritt pro User und Lektion
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
course_id INTEGER NOT NULL,
|
||||||
|
lesson_id INTEGER NOT NULL,
|
||||||
|
completed BOOLEAN DEFAULT false,
|
||||||
|
score INTEGER DEFAULT 0,
|
||||||
|
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
CONSTRAINT vocab_course_progress_user_fk
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_progress_course_fk
|
||||||
|
FOREIGN KEY (course_id)
|
||||||
|
REFERENCES community.vocab_course(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_progress_lesson_fk
|
||||||
|
FOREIGN KEY (lesson_id)
|
||||||
|
REFERENCES community.vocab_course_lesson(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Indizes
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
|
||||||
|
ON community.vocab_course(owner_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
||||||
|
ON community.vocab_course(language_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
||||||
|
ON community.vocab_course(is_public);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
||||||
|
ON community.vocab_course_lesson(course_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
|
||||||
|
ON community.vocab_course_lesson(chapter_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
|
||||||
|
ON community.vocab_course_enrollment(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
|
||||||
|
ON community.vocab_course_enrollment(course_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
|
||||||
|
ON community.vocab_course_progress(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
|
||||||
|
ON community.vocab_course_progress(course_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
|
||||||
|
ON community.vocab_course_progress(lesson_id);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TABLE IF EXISTS community.vocab_course_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS community.vocab_course_enrollment CASCADE;
|
||||||
|
DROP TABLE IF EXISTS community.vocab_course_lesson CASCADE;
|
||||||
|
DROP TABLE IF EXISTS community.vocab_course CASCADE;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// Grammatik-Übungstypen (z.B. "gap_fill", "multiple_choice", "sentence_building", "transformation")
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Grammatik-Übungen (verknüpft mit Lektionen)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
lesson_id INTEGER NOT NULL,
|
||||||
|
exercise_type_id INTEGER NOT NULL,
|
||||||
|
exercise_number INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
instruction TEXT,
|
||||||
|
question_data JSONB NOT NULL,
|
||||||
|
answer_data JSONB NOT NULL,
|
||||||
|
explanation TEXT,
|
||||||
|
created_by_user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
||||||
|
FOREIGN KEY (lesson_id)
|
||||||
|
REFERENCES community.vocab_course_lesson(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_type_fk
|
||||||
|
FOREIGN KEY (exercise_type_id)
|
||||||
|
REFERENCES community.vocab_grammar_exercise_type(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_creator_fk
|
||||||
|
FOREIGN KEY (created_by_user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Fortschritt für Grammatik-Übungen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
exercise_id INTEGER NOT NULL,
|
||||||
|
attempts INTEGER DEFAULT 0,
|
||||||
|
correct_attempts INTEGER DEFAULT 0,
|
||||||
|
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
completed BOOLEAN DEFAULT false,
|
||||||
|
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES community."user"(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
||||||
|
FOREIGN KEY (exercise_id)
|
||||||
|
REFERENCES community.vocab_grammar_exercise(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Indizes
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
||||||
|
ON community.vocab_grammar_exercise(lesson_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
||||||
|
ON community.vocab_grammar_exercise(exercise_type_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
||||||
|
ON community.vocab_grammar_exercise_progress(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
||||||
|
ON community.vocab_grammar_exercise_progress(exercise_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Standard-Übungstypen einfügen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||||
|
('gap_fill', 'Lückentext-Übung'),
|
||||||
|
('multiple_choice', 'Multiple-Choice-Fragen'),
|
||||||
|
('sentence_building', 'Satzbau-Übung'),
|
||||||
|
('transformation', 'Satzumformung'),
|
||||||
|
('conjugation', 'Konjugations-Übung'),
|
||||||
|
('declension', 'Deklinations-Übung')
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TABLE IF EXISTS community.vocab_grammar_exercise_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS community.vocab_grammar_exercise CASCADE;
|
||||||
|
DROP TABLE IF EXISTS community.vocab_grammar_exercise_type CASCADE;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
47
backend/migrations/20260115000002-add-course-structure.cjs
Normal file
47
backend/migrations/20260115000002-add-course-structure.cjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// chapter_id optional machen (nicht alle Lektionen brauchen ein Kapitel)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
ALTER COLUMN chapter_id DROP NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Kurs-Wochen/Module hinzufügen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
ADD COLUMN IF NOT EXISTS week_number INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS day_number INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
|
||||||
|
ADD COLUMN IF NOT EXISTS audio_url TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS cultural_notes TEXT;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Indizes für Wochen/Tage
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
||||||
|
ON community.vocab_course_lesson(course_id, week_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
||||||
|
ON community.vocab_course_lesson(lesson_type);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Kommentar hinzufügen für lesson_type
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
|
||||||
|
'Type: vocab, grammar, conversation, culture, review';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
DROP COLUMN IF EXISTS week_number,
|
||||||
|
DROP COLUMN IF EXISTS day_number,
|
||||||
|
DROP COLUMN IF EXISTS lesson_type,
|
||||||
|
DROP COLUMN IF EXISTS audio_url,
|
||||||
|
DROP COLUMN IF EXISTS cultural_notes;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
// Lernziele für Lektionen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
|
||||||
|
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Kommentare hinzufügen
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
|
||||||
|
'Zielzeit in Minuten für diese Lektion';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
|
||||||
|
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
|
||||||
|
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
DROP COLUMN IF EXISTS target_minutes,
|
||||||
|
DROP COLUMN IF EXISTS target_score_percent,
|
||||||
|
DROP COLUMN IF EXISTS requires_review;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
// Create index on (user_id, shown) to optimize markNotificationsShown queries
|
||||||
|
// This prevents deadlocks by allowing fast lookups and reducing lock contention
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'i'
|
||||||
|
AND c.relname = 'idx_notification_user_id_shown'
|
||||||
|
AND n.nspname = 'falukant_log'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_notification_user_id_shown
|
||||||
|
ON falukant_log.notification (user_id, shown);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP INDEX IF EXISTS falukant_log.idx_notification_user_id_shown;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
19
backend/migrations/add_chat_room_dialog_fields.sql
Normal file
19
backend/migrations/add_chat_room_dialog_fields.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE chat.room
|
||||||
|
ADD COLUMN IF NOT EXISTS gender_restriction_id INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS min_age INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS max_age INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS password VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS friends_of_owner_only BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS required_user_right_id INTEGER;
|
||||||
|
|
||||||
|
UPDATE chat.room
|
||||||
|
SET friends_of_owner_only = FALSE
|
||||||
|
WHERE friends_of_owner_only IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE chat.room
|
||||||
|
ALTER COLUMN friends_of_owner_only SET DEFAULT FALSE,
|
||||||
|
ALTER COLUMN friends_of_owner_only SET NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
20
backend/migrations/add_condition_to_vehicle.sql
Normal file
20
backend/migrations/add_condition_to_vehicle.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Migration: Add condition and available_from columns to vehicle table
|
||||||
|
-- Date: 2024-12-02
|
||||||
|
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ADD COLUMN IF NOT EXISTS condition INTEGER NOT NULL DEFAULT 100;
|
||||||
|
|
||||||
|
ALTER TABLE falukant_data.vehicle
|
||||||
|
ADD COLUMN IF NOT EXISTS available_from TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN falukant_data.vehicle.condition IS 'Current condition of the vehicle (0-100)';
|
||||||
|
COMMENT ON COLUMN falukant_data.vehicle.available_from IS 'Timestamp when the vehicle becomes available for use';
|
||||||
|
|
||||||
|
-- Migration: Add build_time_minutes column to vehicle type table
|
||||||
|
-- Date: 2024-12-03
|
||||||
|
|
||||||
|
ALTER TABLE falukant_type.vehicle
|
||||||
|
ADD COLUMN IF NOT EXISTS build_time_minutes INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN falukant_type.vehicle.build_time_minutes IS 'Time to construct the vehicle, in minutes';
|
||||||
|
|
||||||
9
backend/migrations/add_is_heir_to_child_relation.sql
Normal file
9
backend/migrations/add_is_heir_to_child_relation.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- Migration: Add is_heir column to child_relation table
|
||||||
|
-- Date: 2025-12-08
|
||||||
|
-- Description: Adds a boolean field to mark a child as the heir
|
||||||
|
|
||||||
|
ALTER TABLE falukant_data.child_relation
|
||||||
|
ADD COLUMN IF NOT EXISTS is_heir BOOLEAN DEFAULT false;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN falukant_data.child_relation.is_heir IS 'Marks whether this child is set as the heir';
|
||||||
|
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
up: async (queryInterface, Sequelize) => {
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
// Aktiviere die pgcrypto Erweiterung, die die digest() Funktion bereitstellt
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
`);
|
||||||
|
|
||||||
await queryInterface.sequelize.query(`
|
await queryInterface.sequelize.query(`
|
||||||
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
|
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
7
backend/migrations/make_transport_product_nullable.sql
Normal file
7
backend/migrations/make_transport_product_nullable.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Migration: Make productId and size nullable in transport table
|
||||||
|
-- This allows empty transports (moving vehicles without products)
|
||||||
|
|
||||||
|
ALTER TABLE falukant_data.transport
|
||||||
|
ALTER COLUMN product_id DROP NOT NULL,
|
||||||
|
ALTER COLUMN size DROP NOT NULL;
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import ChatUser from './chat/user.js';
|
|||||||
import Room from './chat/room.js';
|
import Room from './chat/room.js';
|
||||||
import User from './community/user.js';
|
import User from './community/user.js';
|
||||||
import UserParam from './community/user_param.js';
|
import UserParam from './community/user_param.js';
|
||||||
|
import UserDashboard from './community/user_dashboard.js';
|
||||||
import UserParamType from './type/user_param.js';
|
import UserParamType from './type/user_param.js';
|
||||||
import UserRightType from './type/user_right.js';
|
import UserRightType from './type/user_right.js';
|
||||||
import UserRight from './community/user_right.js';
|
import UserRight from './community/user_right.js';
|
||||||
@@ -44,6 +45,7 @@ import FalukantStockType from './falukant/type/stock.js';
|
|||||||
import Knowledge from './falukant/data/product_knowledge.js';
|
import Knowledge from './falukant/data/product_knowledge.js';
|
||||||
import ProductType from './falukant/type/product.js';
|
import ProductType from './falukant/type/product.js';
|
||||||
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
||||||
|
import TitleBenefit from './falukant/type/title_benefit.js';
|
||||||
import TitleRequirement from './falukant/type/title_requirement.js';
|
import TitleRequirement from './falukant/type/title_requirement.js';
|
||||||
import Branch from './falukant/data/branch.js';
|
import Branch from './falukant/data/branch.js';
|
||||||
import BranchType from './falukant/type/branch.js';
|
import BranchType from './falukant/type/branch.js';
|
||||||
@@ -93,10 +95,30 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
|
|||||||
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
||||||
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
|
||||||
import ElectionHistory from './falukant/log/election_history.js';
|
import ElectionHistory from './falukant/log/election_history.js';
|
||||||
|
import ChurchOfficeType from './falukant/type/church_office_type.js';
|
||||||
|
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
|
||||||
|
import ChurchOffice from './falukant/data/church_office.js';
|
||||||
|
import ChurchApplication from './falukant/data/church_application.js';
|
||||||
import Underground from './falukant/data/underground.js';
|
import Underground from './falukant/data/underground.js';
|
||||||
import UndergroundType from './falukant/type/underground.js';
|
import UndergroundType from './falukant/type/underground.js';
|
||||||
|
import VehicleType from './falukant/type/vehicle.js';
|
||||||
|
import Vehicle from './falukant/data/vehicle.js';
|
||||||
|
import Transport from './falukant/data/transport.js';
|
||||||
|
import RegionDistance from './falukant/data/region_distance.js';
|
||||||
|
import WeatherType from './falukant/type/weather.js';
|
||||||
|
import Weather from './falukant/data/weather.js';
|
||||||
|
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
|
||||||
|
import ProductPriceHistory from './falukant/log/product_price_history.js';
|
||||||
import Blog from './community/blog.js';
|
import Blog from './community/blog.js';
|
||||||
import BlogPost from './community/blog_post.js';
|
import BlogPost from './community/blog_post.js';
|
||||||
|
import VocabCourse from './community/vocab_course.js';
|
||||||
|
import VocabCourseLesson from './community/vocab_course_lesson.js';
|
||||||
|
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
|
||||||
|
import VocabCourseProgress from './community/vocab_course_progress.js';
|
||||||
|
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
||||||
|
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
||||||
|
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
||||||
|
import CalendarEvent from './community/calendar_event.js';
|
||||||
import Campaign from './match3/campaign.js';
|
import Campaign from './match3/campaign.js';
|
||||||
import Match3Level from './match3/level.js';
|
import Match3Level from './match3/level.js';
|
||||||
import Objective from './match3/objective.js';
|
import Objective from './match3/objective.js';
|
||||||
@@ -148,6 +170,9 @@ export default function setupAssociations() {
|
|||||||
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
||||||
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
|
||||||
|
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
|
||||||
|
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
|
||||||
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
|
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
|
||||||
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
|
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
|
||||||
|
|
||||||
@@ -284,6 +309,21 @@ export default function setupAssociations() {
|
|||||||
RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' });
|
RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' });
|
||||||
RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' });
|
RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' });
|
||||||
|
|
||||||
|
Weather.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
RegionData.hasOne(Weather, { foreignKey: 'regionId', as: 'weather' });
|
||||||
|
|
||||||
|
Weather.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||||
|
WeatherType.hasMany(Weather, { foreignKey: 'weatherTypeId', as: 'weathers' });
|
||||||
|
|
||||||
|
ProductWeatherEffect.belongsTo(ProductType, { foreignKey: 'productId', as: 'product' });
|
||||||
|
ProductType.hasMany(ProductWeatherEffect, { foreignKey: 'productId', as: 'weatherEffects' });
|
||||||
|
|
||||||
|
ProductWeatherEffect.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||||
|
WeatherType.hasMany(ProductWeatherEffect, { foreignKey: 'weatherTypeId', as: 'productEffects' });
|
||||||
|
|
||||||
|
Production.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
|
||||||
|
WeatherType.hasMany(Production, { foreignKey: 'weatherTypeId', as: 'productions' });
|
||||||
|
|
||||||
FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' });
|
FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' });
|
||||||
RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' });
|
RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' });
|
||||||
|
|
||||||
@@ -313,6 +353,8 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
||||||
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
|
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
|
||||||
|
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
|
||||||
|
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
||||||
|
|
||||||
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
|
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
|
||||||
@@ -383,6 +425,13 @@ export default function setupAssociations() {
|
|||||||
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
|
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
|
||||||
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
|
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
|
||||||
|
|
||||||
|
// Produkt-Preishistorie (Zeitreihe für Preiskurven)
|
||||||
|
ProductPriceHistory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
|
||||||
|
ProductType.hasMany(ProductPriceHistory, { foreignKey: 'productId', as: 'priceHistory' });
|
||||||
|
|
||||||
|
ProductPriceHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
RegionData.hasMany(ProductPriceHistory, { foreignKey: 'regionId', as: 'productPriceHistory' });
|
||||||
|
|
||||||
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
|
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
|
||||||
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
|
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
|
||||||
|
|
||||||
@@ -421,6 +470,89 @@ export default function setupAssociations() {
|
|||||||
PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' });
|
PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' });
|
||||||
FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' });
|
FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' });
|
||||||
|
|
||||||
|
// Vehicles & Transports
|
||||||
|
|
||||||
|
VehicleType.hasMany(Vehicle, {
|
||||||
|
foreignKey: 'vehicleTypeId',
|
||||||
|
as: 'vehicles',
|
||||||
|
});
|
||||||
|
Vehicle.belongsTo(VehicleType, {
|
||||||
|
foreignKey: 'vehicleTypeId',
|
||||||
|
as: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
FalukantUser.hasMany(Vehicle, {
|
||||||
|
foreignKey: 'falukantUserId',
|
||||||
|
as: 'vehicles',
|
||||||
|
});
|
||||||
|
Vehicle.belongsTo(FalukantUser, {
|
||||||
|
foreignKey: 'falukantUserId',
|
||||||
|
as: 'owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
RegionData.hasMany(Vehicle, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'vehicles',
|
||||||
|
});
|
||||||
|
Vehicle.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Region distances
|
||||||
|
RegionData.hasMany(RegionDistance, {
|
||||||
|
foreignKey: 'sourceRegionId',
|
||||||
|
as: 'distancesFrom',
|
||||||
|
});
|
||||||
|
RegionData.hasMany(RegionDistance, {
|
||||||
|
foreignKey: 'targetRegionId',
|
||||||
|
as: 'distancesTo',
|
||||||
|
});
|
||||||
|
RegionDistance.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'sourceRegionId',
|
||||||
|
as: 'sourceRegion',
|
||||||
|
});
|
||||||
|
RegionDistance.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'targetRegionId',
|
||||||
|
as: 'targetRegion',
|
||||||
|
});
|
||||||
|
|
||||||
|
Transport.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'sourceRegionId',
|
||||||
|
as: 'sourceRegion',
|
||||||
|
});
|
||||||
|
Transport.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'targetRegionId',
|
||||||
|
as: 'targetRegion',
|
||||||
|
});
|
||||||
|
|
||||||
|
RegionData.hasMany(Transport, {
|
||||||
|
foreignKey: 'sourceRegionId',
|
||||||
|
as: 'outgoingTransports',
|
||||||
|
});
|
||||||
|
RegionData.hasMany(Transport, {
|
||||||
|
foreignKey: 'targetRegionId',
|
||||||
|
as: 'incomingTransports',
|
||||||
|
});
|
||||||
|
|
||||||
|
Transport.belongsTo(ProductType, {
|
||||||
|
foreignKey: 'productId',
|
||||||
|
as: 'productType',
|
||||||
|
});
|
||||||
|
ProductType.hasMany(Transport, {
|
||||||
|
foreignKey: 'productId',
|
||||||
|
as: 'transports',
|
||||||
|
});
|
||||||
|
|
||||||
|
Transport.belongsTo(Vehicle, {
|
||||||
|
foreignKey: 'vehicleId',
|
||||||
|
as: 'vehicle',
|
||||||
|
});
|
||||||
|
Vehicle.hasMany(Transport, {
|
||||||
|
foreignKey: 'vehicleId',
|
||||||
|
as: 'transports',
|
||||||
|
});
|
||||||
|
|
||||||
PromotionalGift.hasMany(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' });
|
PromotionalGift.hasMany(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' });
|
||||||
PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' });
|
PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' });
|
||||||
|
|
||||||
@@ -453,14 +585,14 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
Party.belongsToMany(TitleOfNobility, {
|
Party.belongsToMany(TitleOfNobility, {
|
||||||
through: PartyInvitedNobility,
|
through: PartyInvitedNobility,
|
||||||
foreignKey: 'party_id',
|
foreignKey: 'partyId',
|
||||||
otherKey: 'title_of_nobility_id',
|
otherKey: 'titleOfNobilityId',
|
||||||
as: 'invitedNobilities',
|
as: 'invitedNobilities',
|
||||||
});
|
});
|
||||||
TitleOfNobility.belongsToMany(Party, {
|
TitleOfNobility.belongsToMany(Party, {
|
||||||
through: PartyInvitedNobility,
|
through: PartyInvitedNobility,
|
||||||
foreignKey: 'title_of_nobility_id',
|
foreignKey: 'titleOfNobilityId',
|
||||||
otherKey: 'party_id',
|
otherKey: 'partyId',
|
||||||
as: 'partiesInvitedTo',
|
as: 'partiesInvitedTo',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -493,44 +625,52 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
Learning.belongsTo(LearnRecipient, {
|
Learning.belongsTo(LearnRecipient, {
|
||||||
foreignKey: 'learningRecipientId',
|
foreignKey: 'learningRecipientId',
|
||||||
as: 'recipient'
|
as: 'recipient',
|
||||||
|
constraints: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
LearnRecipient.hasMany(Learning, {
|
LearnRecipient.hasMany(Learning, {
|
||||||
foreignKey: 'learningRecipientId',
|
foreignKey: 'learningRecipientId',
|
||||||
as: 'learnings'
|
as: 'learnings',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Learning.belongsTo(FalukantUser, {
|
Learning.belongsTo(FalukantUser, {
|
||||||
foreignKey: 'associatedFalukantUserId',
|
foreignKey: 'associatedFalukantUserId',
|
||||||
as: 'learner'
|
as: 'learner',
|
||||||
|
constraints: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
FalukantUser.hasMany(Learning, {
|
FalukantUser.hasMany(Learning, {
|
||||||
foreignKey: 'associatedFalukantUserId',
|
foreignKey: 'associatedFalukantUserId',
|
||||||
as: 'learnings'
|
as: 'learnings',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Learning.belongsTo(ProductType, {
|
Learning.belongsTo(ProductType, {
|
||||||
foreignKey: 'productId',
|
foreignKey: 'productId',
|
||||||
as: 'productType'
|
as: 'productType',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
ProductType.hasMany(Learning, {
|
ProductType.hasMany(Learning, {
|
||||||
foreignKey: 'productId',
|
foreignKey: 'productId',
|
||||||
as: 'learnings'
|
as: 'learnings',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Learning.belongsTo(FalukantCharacter, {
|
Learning.belongsTo(FalukantCharacter, {
|
||||||
foreignKey: 'associatedLearningCharacterId',
|
foreignKey: 'associatedLearningCharacterId',
|
||||||
as: 'learningCharacter'
|
as: 'learningCharacter',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
FalukantCharacter.hasMany(Learning, {
|
FalukantCharacter.hasMany(Learning, {
|
||||||
foreignKey: 'associatedLearningCharacterId',
|
foreignKey: 'associatedLearningCharacterId',
|
||||||
as: 'learningsCharacter'
|
as: 'learningsCharacter',
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
FalukantUser.hasMany(Credit, {
|
FalukantUser.hasMany(Credit, {
|
||||||
@@ -746,6 +886,96 @@ export default function setupAssociations() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// — Church Offices —
|
||||||
|
|
||||||
|
// Requirements for church office
|
||||||
|
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeType'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'requirements'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prerequisite office type
|
||||||
|
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'prerequisiteOfficeTypeId',
|
||||||
|
as: 'prerequisiteOfficeType'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actual church office holdings
|
||||||
|
ChurchOffice.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'type'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchOffice, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'offices'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'holder'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasOne(ChurchOffice, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'heldChurchOffice'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Supervisor relationship
|
||||||
|
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisor'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Region relationship
|
||||||
|
ChurchOffice.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region'
|
||||||
|
});
|
||||||
|
RegionData.hasMany(ChurchOffice, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'churchOffices'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Applications for church office
|
||||||
|
ChurchApplication.belongsTo(ChurchOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeType'
|
||||||
|
});
|
||||||
|
ChurchOfficeType.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'applications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'applicant'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'churchApplications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisor'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'supervisorId',
|
||||||
|
as: 'supervisedApplications'
|
||||||
|
});
|
||||||
|
|
||||||
|
ChurchApplication.belongsTo(RegionData, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'region'
|
||||||
|
});
|
||||||
|
RegionData.hasMany(ChurchApplication, {
|
||||||
|
foreignKey: 'regionId',
|
||||||
|
as: 'churchApplications'
|
||||||
|
});
|
||||||
|
|
||||||
Underground.belongsTo(UndergroundType, {
|
Underground.belongsTo(UndergroundType, {
|
||||||
foreignKey: 'undergroundTypeId',
|
foreignKey: 'undergroundTypeId',
|
||||||
as: 'undergroundType'
|
as: 'undergroundType'
|
||||||
@@ -828,5 +1058,41 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
|
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
|
||||||
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
|
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
|
||||||
|
|
||||||
|
// Vocab Course associations
|
||||||
|
VocabCourse.belongsTo(User, { foreignKey: 'ownerUserId', as: 'owner' });
|
||||||
|
User.hasMany(VocabCourse, { foreignKey: 'ownerUserId', as: 'ownedCourses' });
|
||||||
|
|
||||||
|
VocabCourseLesson.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
||||||
|
VocabCourse.hasMany(VocabCourseLesson, { foreignKey: 'courseId', as: 'lessons' });
|
||||||
|
|
||||||
|
VocabCourseEnrollment.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
User.hasMany(VocabCourseEnrollment, { foreignKey: 'userId', as: 'courseEnrollments' });
|
||||||
|
VocabCourseEnrollment.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
||||||
|
VocabCourse.hasMany(VocabCourseEnrollment, { foreignKey: 'courseId', as: 'enrollments' });
|
||||||
|
|
||||||
|
VocabCourseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
User.hasMany(VocabCourseProgress, { foreignKey: 'userId', as: 'courseProgress' });
|
||||||
|
VocabCourseProgress.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
||||||
|
VocabCourse.hasMany(VocabCourseProgress, { foreignKey: 'courseId', as: 'progress' });
|
||||||
|
VocabCourseProgress.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
||||||
|
VocabCourseLesson.hasMany(VocabCourseProgress, { foreignKey: 'lessonId', as: 'progress' });
|
||||||
|
|
||||||
|
// Grammar Exercise associations
|
||||||
|
VocabGrammarExercise.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
||||||
|
VocabCourseLesson.hasMany(VocabGrammarExercise, { foreignKey: 'lessonId', as: 'grammarExercises' });
|
||||||
|
VocabGrammarExercise.belongsTo(VocabGrammarExerciseType, { foreignKey: 'exerciseTypeId', as: 'exerciseType' });
|
||||||
|
VocabGrammarExerciseType.hasMany(VocabGrammarExercise, { foreignKey: 'exerciseTypeId', as: 'exercises' });
|
||||||
|
VocabGrammarExercise.belongsTo(User, { foreignKey: 'createdByUserId', as: 'creator' });
|
||||||
|
User.hasMany(VocabGrammarExercise, { foreignKey: 'createdByUserId', as: 'createdGrammarExercises' });
|
||||||
|
|
||||||
|
VocabGrammarExerciseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
|
||||||
|
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
|
||||||
|
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
|
||||||
|
|
||||||
|
// Calendar associations
|
||||||
|
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
backend/models/community/calendar_event.js
Normal file
86
backend/models/community/calendar_event.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class CalendarEvent extends Model { }
|
||||||
|
|
||||||
|
CalendarEvent.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'user',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING(255),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
categoryId: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'personal',
|
||||||
|
comment: 'Category key: personal, work, family, health, birthday, holiday, reminder, other'
|
||||||
|
},
|
||||||
|
startDate: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'End date for multi-day events, null means same as startDate'
|
||||||
|
},
|
||||||
|
startTime: {
|
||||||
|
type: DataTypes.TIME,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Start time, null for all-day events'
|
||||||
|
},
|
||||||
|
endTime: {
|
||||||
|
type: DataTypes.TIME,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'End time, null for all-day events'
|
||||||
|
},
|
||||||
|
allDay: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'CalendarEvent',
|
||||||
|
tableName: 'calendar_event',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['user_id']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['user_id', 'start_date']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['user_id', 'start_date', 'end_date']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CalendarEvent;
|
||||||
@@ -8,16 +8,12 @@ const Folder = sequelize.define('folder', {
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
parentId: {
|
parentId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true
|
||||||
references: {
|
},
|
||||||
model: 'folder',
|
|
||||||
key: 'id'}},
|
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
}}, {
|
||||||
model: 'user',
|
|
||||||
key: 'id'}}}, {
|
|
||||||
tableName: 'folder',
|
tableName: 'folder',
|
||||||
schema: 'community',
|
schema: 'community',
|
||||||
underscored: true,
|
underscored: true,
|
||||||
|
|||||||
@@ -10,22 +10,11 @@ const FolderImageVisibility = sequelize.define('folder_image_visibility', {
|
|||||||
},
|
},
|
||||||
folderId: {
|
folderId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: 'folder',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
visibilityTypeId: {
|
visibilityTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: {
|
|
||||||
schema: 'type',
|
|
||||||
tableName: 'image_visibility_type'
|
|
||||||
},
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'folder_image_visibility',
|
tableName: 'folder_image_visibility',
|
||||||
|
|||||||
@@ -10,19 +10,11 @@ const FolderVisibilityUser = sequelize.define('folder_visibility_user', {
|
|||||||
},
|
},
|
||||||
folderId: {
|
folderId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: 'folder',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
visibilityUserId: {
|
visibilityUserId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: 'image_visibility_user',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'folder_visibility_user',
|
tableName: 'folder_visibility_user',
|
||||||
|
|||||||
@@ -10,19 +10,11 @@ const GuestbookEntry = sequelize.define('guestbook_entry', {
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
recipientId: {
|
recipientId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: User,
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
senderId: {
|
senderId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true
|
||||||
references: {
|
|
||||||
model: User,
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
senderUsername: {
|
senderUsername: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
|
|||||||
@@ -18,16 +18,12 @@ const Image = sequelize.define('image', {
|
|||||||
unique: true},
|
unique: true},
|
||||||
folderId: {
|
folderId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
},
|
||||||
model: 'folder',
|
|
||||||
key: 'id'}},
|
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
}}, {
|
||||||
model: 'user',
|
|
||||||
key: 'id'}}}, {
|
|
||||||
tableName: 'image',
|
tableName: 'image',
|
||||||
schema: 'community',
|
schema: 'community',
|
||||||
underscored: true,
|
underscored: true,
|
||||||
|
|||||||
@@ -10,22 +10,11 @@ const ImageImageVisibility = sequelize.define('image_image_visibility', {
|
|||||||
},
|
},
|
||||||
imageId: {
|
imageId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: 'image',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
visibilityTypeId: {
|
visibilityTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: {
|
|
||||||
schema: 'type',
|
|
||||||
tableName: 'image_visibility_type'
|
|
||||||
},
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'image_image_visibility',
|
tableName: 'image_image_visibility',
|
||||||
|
|||||||
24
backend/models/community/user_dashboard.js
Normal file
24
backend/models/community/user_dashboard.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import User from './user.js';
|
||||||
|
|
||||||
|
const UserDashboard = sequelize.define('user_dashboard', {
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
references: { model: User, key: 'id' }
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: { widgets: [] }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'user_dashboard',
|
||||||
|
schema: 'community',
|
||||||
|
underscored: true,
|
||||||
|
timestamps: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserDashboard;
|
||||||
@@ -7,19 +7,11 @@ import { encrypt, decrypt } from '../../utils/encryption.js';
|
|||||||
const UserParam = sequelize.define('user_param', {
|
const UserParam = sequelize.define('user_param', {
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: User,
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
paramTypeId: {
|
paramTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: UserParamType,
|
|
||||||
key: 'id',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
|
|||||||
@@ -6,19 +6,11 @@ import UserRightType from '../type/user_right.js';
|
|||||||
const UserRight = sequelize.define('user_right', {
|
const UserRight = sequelize.define('user_right', {
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: User,
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
rightTypeId: {
|
rightTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
references: {
|
|
||||||
model: UserRightType,
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
}}, {
|
}}, {
|
||||||
tableName: 'user_right',
|
tableName: 'user_right',
|
||||||
schema: 'community',
|
schema: 'community',
|
||||||
|
|||||||
75
backend/models/community/vocab_course.js
Normal file
75
backend/models/community/vocab_course.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabCourse extends Model {}
|
||||||
|
|
||||||
|
VocabCourse.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
ownerUserId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'owner_user_id'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
languageId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'language_id'
|
||||||
|
},
|
||||||
|
nativeLanguageId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'native_language_id',
|
||||||
|
comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'
|
||||||
|
},
|
||||||
|
difficultyLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1,
|
||||||
|
field: 'difficulty_level'
|
||||||
|
},
|
||||||
|
isPublic: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
field: 'is_public'
|
||||||
|
},
|
||||||
|
shareCode: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
|
field: 'share_code'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'updated_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabCourse',
|
||||||
|
tableName: 'vocab_course',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabCourse;
|
||||||
37
backend/models/community/vocab_course_enrollment.js
Normal file
37
backend/models/community/vocab_course_enrollment.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabCourseEnrollment extends Model {}
|
||||||
|
|
||||||
|
VocabCourseEnrollment.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'user_id'
|
||||||
|
},
|
||||||
|
courseId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'course_id'
|
||||||
|
},
|
||||||
|
enrolledAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'enrolled_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabCourseEnrollment',
|
||||||
|
tableName: 'vocab_course_enrollment',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabCourseEnrollment;
|
||||||
93
backend/models/community/vocab_course_lesson.js
Normal file
93
backend/models/community/vocab_course_lesson.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabCourseLesson extends Model {}
|
||||||
|
|
||||||
|
VocabCourseLesson.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
courseId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'course_id'
|
||||||
|
},
|
||||||
|
chapterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'chapter_id'
|
||||||
|
},
|
||||||
|
lessonNumber: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'lesson_number'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
weekNumber: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'week_number'
|
||||||
|
},
|
||||||
|
dayNumber: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'day_number'
|
||||||
|
},
|
||||||
|
lessonType: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'vocab',
|
||||||
|
field: 'lesson_type'
|
||||||
|
},
|
||||||
|
audioUrl: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'audio_url'
|
||||||
|
},
|
||||||
|
culturalNotes: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'cultural_notes'
|
||||||
|
},
|
||||||
|
targetMinutes: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'target_minutes'
|
||||||
|
},
|
||||||
|
targetScorePercent: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 80,
|
||||||
|
field: 'target_score_percent'
|
||||||
|
},
|
||||||
|
requiresReview: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
field: 'requires_review'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabCourseLesson',
|
||||||
|
tableName: 'vocab_course_lesson',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabCourseLesson;
|
||||||
56
backend/models/community/vocab_course_progress.js
Normal file
56
backend/models/community/vocab_course_progress.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabCourseProgress extends Model {}
|
||||||
|
|
||||||
|
VocabCourseProgress.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'user_id'
|
||||||
|
},
|
||||||
|
courseId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'course_id'
|
||||||
|
},
|
||||||
|
lessonId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'lesson_id'
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
lastAccessedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'last_accessed_at'
|
||||||
|
},
|
||||||
|
completedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'completed_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabCourseProgress',
|
||||||
|
tableName: 'vocab_course_progress',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabCourseProgress;
|
||||||
69
backend/models/community/vocab_grammar_exercise.js
Normal file
69
backend/models/community/vocab_grammar_exercise.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabGrammarExercise extends Model {}
|
||||||
|
|
||||||
|
VocabGrammarExercise.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
lessonId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'lesson_id'
|
||||||
|
},
|
||||||
|
exerciseTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'exercise_type_id'
|
||||||
|
},
|
||||||
|
exerciseNumber: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'exercise_number'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
instruction: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
questionData: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'question_data'
|
||||||
|
},
|
||||||
|
answerData: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'answer_data'
|
||||||
|
},
|
||||||
|
explanation: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdByUserId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'created_by_user_id'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabGrammarExercise',
|
||||||
|
tableName: 'vocab_grammar_exercise',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabGrammarExercise;
|
||||||
57
backend/models/community/vocab_grammar_exercise_progress.js
Normal file
57
backend/models/community/vocab_grammar_exercise_progress.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabGrammarExerciseProgress extends Model {}
|
||||||
|
|
||||||
|
VocabGrammarExerciseProgress.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'user_id'
|
||||||
|
},
|
||||||
|
exerciseId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'exercise_id'
|
||||||
|
},
|
||||||
|
attempts: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
correctAttempts: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'correct_attempts'
|
||||||
|
},
|
||||||
|
lastAttemptAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'last_attempt_at'
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
completedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'completed_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabGrammarExerciseProgress',
|
||||||
|
tableName: 'vocab_grammar_exercise_progress',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabGrammarExerciseProgress;
|
||||||
36
backend/models/community/vocab_grammar_exercise_type.js
Normal file
36
backend/models/community/vocab_grammar_exercise_type.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabGrammarExerciseType extends Model {}
|
||||||
|
|
||||||
|
VocabGrammarExerciseType.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabGrammarExerciseType',
|
||||||
|
tableName: 'vocab_grammar_exercise_type',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabGrammarExerciseType;
|
||||||
@@ -34,6 +34,18 @@ FalukantCharacter.init(
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 1}
|
defaultValue: 1}
|
||||||
|
,
|
||||||
|
reputation: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
// Initialisierung: zufällig 20..80 (Prozent)
|
||||||
|
// DB-seitig per DEFAULT umgesetzt, damit es auch ohne App-Logic gilt.
|
||||||
|
defaultValue: sequelize.literal('(floor(random()*61)+20)'),
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
47
backend/models/falukant/data/church_application.js
Normal file
47
backend/models/falukant/data/church_application.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchApplication extends Model {}
|
||||||
|
|
||||||
|
ChurchApplication.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
supervisorId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'ID des Vorgesetzten, der über die Bewerbung entscheidet (null für Einstiegspositionen ohne Supervisor)'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'pending'
|
||||||
|
},
|
||||||
|
decisionDate: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchApplication',
|
||||||
|
tableName: 'church_application',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchApplication;
|
||||||
38
backend/models/falukant/data/church_office.js
Normal file
38
backend/models/falukant/data/church_office.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOffice extends Model {}
|
||||||
|
|
||||||
|
ChurchOffice.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
supervisorId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'ID des Vorgesetzten (höhere Position in der Hierarchie)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOffice',
|
||||||
|
tableName: 'church_office',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOffice;
|
||||||
@@ -29,6 +29,10 @@ Director.init({
|
|||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true},
|
defaultValue: true},
|
||||||
|
mayRepairVehicles: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true},
|
||||||
lastSalaryPayout: {
|
lastSalaryPayout: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Model, DataTypes } from 'sequelize';
|
import { Model, DataTypes } from 'sequelize';
|
||||||
import { sequelize } from '../../../utils/sequelize.js';
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
import WeatherType from '../type/weather.js';
|
||||||
|
|
||||||
class Production extends Model { }
|
class Production extends Model { }
|
||||||
|
|
||||||
@@ -13,10 +14,20 @@ Production.init({
|
|||||||
quantity: {
|
quantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
|
weatherTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Wetter zum Zeitpunkt der Produktionserstellung'
|
||||||
|
},
|
||||||
startTimestamp: {
|
startTimestamp: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')}
|
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')},
|
||||||
|
sleep: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: 'Produktion ist zurückgestellt'}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'Production',
|
modelName: 'Production',
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ RegionData.init({
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
taxPercent: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 7,
|
||||||
|
field: 'tax_percent'
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'RegionData',
|
modelName: 'RegionData',
|
||||||
|
|||||||
41
backend/models/falukant/data/region_distance.js
Normal file
41
backend/models/falukant/data/region_distance.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
import RegionData from './region.js';
|
||||||
|
|
||||||
|
class RegionDistance extends Model {}
|
||||||
|
|
||||||
|
RegionDistance.init(
|
||||||
|
{
|
||||||
|
sourceRegionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
targetRegionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
transportMode: {
|
||||||
|
// e.g. 'land', 'water', 'air' – should match VehicleType.transportMode
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
distance: {
|
||||||
|
// distance between regions (e.g. in abstract units, used for travel time etc.)
|
||||||
|
type: DataTypes.DOUBLE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'RegionDistance',
|
||||||
|
tableName: 'region_distance',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RegionDistance;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -8,18 +8,10 @@ Relationship.init(
|
|||||||
{
|
{
|
||||||
character1Id: {
|
character1Id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false},
|
||||||
references: {
|
|
||||||
model: FalukantCharacter,
|
|
||||||
key: 'id'},
|
|
||||||
onDelete: 'CASCADE'},
|
|
||||||
character2Id: {
|
character2Id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false},
|
||||||
references: {
|
|
||||||
model: FalukantCharacter,
|
|
||||||
key: 'id'},
|
|
||||||
onDelete: 'CASCADE'},
|
|
||||||
relationshipTypeId: {
|
relationshipTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ FalukantStock.init({
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
quantity: {
|
quantity: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false}}, {
|
allowNull: false},
|
||||||
|
productQuality: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Quality of the stored product (0-100)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'StockData',
|
modelName: 'StockData',
|
||||||
tableName: 'stock',
|
tableName: 'stock',
|
||||||
|
|||||||
41
backend/models/falukant/data/transport.js
Normal file
41
backend/models/falukant/data/transport.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class Transport extends Model {}
|
||||||
|
|
||||||
|
Transport.init(
|
||||||
|
{
|
||||||
|
sourceRegionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
targetRegionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
productId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true, // Nullable für leere Transporte (nur Fahrzeuge bewegen)
|
||||||
|
},
|
||||||
|
vehicleId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Transport',
|
||||||
|
tableName: 'transport',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Transport;
|
||||||
|
|
||||||
|
|
||||||
@@ -8,13 +8,6 @@ FalukantUser.init({
|
|||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
|
||||||
model: {
|
|
||||||
tableName: 'user',
|
|
||||||
schema: 'community'
|
|
||||||
},
|
|
||||||
key: 'id'
|
|
||||||
},
|
|
||||||
unique: true},
|
unique: true},
|
||||||
money: {
|
money: {
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
@@ -38,12 +31,11 @@ FalukantUser.init({
|
|||||||
defaultValue: 1},
|
defaultValue: 1},
|
||||||
mainBranchRegionId: {
|
mainBranchRegionId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true
|
||||||
references: {
|
},
|
||||||
model: RegionData,
|
lastNobilityAdvanceAt: {
|
||||||
key: 'id',
|
type: DataTypes.DATE,
|
||||||
schema: 'falukant_data'
|
allowNull: true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ UserHouse.init({
|
|||||||
},
|
},
|
||||||
houseTypeId: {
|
houseTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
defaultValue: 1
|
|
||||||
},
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false
|
||||||
defaultValue: 1
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
45
backend/models/falukant/data/vehicle.js
Normal file
45
backend/models/falukant/data/vehicle.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class Vehicle extends Model {}
|
||||||
|
|
||||||
|
Vehicle.init(
|
||||||
|
{
|
||||||
|
vehicleTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
falukantUserId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
// current condition of the vehicle (0–100)
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 100,
|
||||||
|
},
|
||||||
|
availableFrom: {
|
||||||
|
// timestamp when the vehicle becomes available for use
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Vehicle',
|
||||||
|
tableName: 'vehicle',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Vehicle;
|
||||||
|
|
||||||
|
|
||||||
30
backend/models/falukant/data/weather.js
Normal file
30
backend/models/falukant/data/weather.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
import RegionData from './region.js';
|
||||||
|
import WeatherType from '../type/weather.js';
|
||||||
|
|
||||||
|
class Weather extends Model {}
|
||||||
|
|
||||||
|
Weather.init(
|
||||||
|
{
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
weatherTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Weather',
|
||||||
|
tableName: 'weather',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Weather;
|
||||||
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Model, DataTypes } from 'sequelize';
|
import { Model, DataTypes } from 'sequelize';
|
||||||
import { sequelize } from '../../../utils/sequelize.js';
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
class Notification extends Model { }
|
class Notification extends Model {
|
||||||
|
// Getter für characterName - wird nicht synchronisiert, da es kein Datenbankfeld ist
|
||||||
|
get characterName() {
|
||||||
|
return this.getDataValue('character_name') || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Notification.init({
|
Notification.init({
|
||||||
userId: {
|
userId: {
|
||||||
@@ -10,6 +15,11 @@ Notification.init({
|
|||||||
tr: {
|
tr: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
|
character_name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'character_name'
|
||||||
|
},
|
||||||
shown: {
|
shown: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
44
backend/models/falukant/log/product_price_history.js
Normal file
44
backend/models/falukant/log/product_price_history.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preishistorie pro Produkt und Region (Zeitreihe für Preis-Graphen).
|
||||||
|
* Aktuell wird diese Tabelle noch nicht befüllt; sie dient nur als Grundlage.
|
||||||
|
*/
|
||||||
|
class ProductPriceHistory extends Model { }
|
||||||
|
|
||||||
|
ProductPriceHistory.init({
|
||||||
|
productId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: DataTypes.DECIMAL(12, 2),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
recordedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ProductPriceHistory',
|
||||||
|
tableName: 'product_price_history',
|
||||||
|
schema: 'falukant_log',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: 'product_price_history_product_region_recorded_idx',
|
||||||
|
fields: ['product_id', 'region_id', 'recorded_at']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ProductPriceHistory;
|
||||||
|
|
||||||
49
backend/models/falukant/log/relationship_change_log.js
Normal file
49
backend/models/falukant/log/relationship_change_log.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log aller Änderungen an relationship und marriage_proposals.
|
||||||
|
* Einträge werden ausschließlich durch DB-Trigger geschrieben und nicht gelöscht.
|
||||||
|
* Hilft zu analysieren, warum z.B. Werbungen um einen Partner verschwinden.
|
||||||
|
*/
|
||||||
|
class RelationshipChangeLog extends Model {}
|
||||||
|
|
||||||
|
RelationshipChangeLog.init(
|
||||||
|
{
|
||||||
|
changedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
},
|
||||||
|
tableName: {
|
||||||
|
type: DataTypes.STRING(64),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
type: DataTypes.STRING(16),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
recordId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
payloadOld: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
payloadNew: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'RelationshipChangeLog',
|
||||||
|
tableName: 'relationship_change_log',
|
||||||
|
schema: 'falukant_log',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RelationshipChangeLog;
|
||||||
59
backend/models/falukant/log/reputation_action.js
Normal file
59
backend/models/falukant/log/reputation_action.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ReputationActionLog extends Model {}
|
||||||
|
|
||||||
|
ReputationActionLog.init(
|
||||||
|
{
|
||||||
|
falukantUserId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'falukant_user_id',
|
||||||
|
},
|
||||||
|
actionTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'action_type_id',
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
baseGain: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'base_gain',
|
||||||
|
},
|
||||||
|
gain: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
timesUsedBefore: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'times_used_before',
|
||||||
|
},
|
||||||
|
actionTimestamp: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: sequelize.literal('CURRENT_TIMESTAMP'),
|
||||||
|
field: 'action_timestamp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ReputationActionLog',
|
||||||
|
tableName: 'reputation_action',
|
||||||
|
schema: 'falukant_log',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
indexes: [
|
||||||
|
{ fields: ['falukant_user_id', 'action_type_id'] },
|
||||||
|
{ fields: ['action_timestamp'] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ReputationActionLog;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOfficeRequirement extends Model {}
|
||||||
|
|
||||||
|
ChurchOfficeRequirement.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
prerequisiteOfficeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Erforderliche niedrigere Position in der Hierarchie'
|
||||||
|
},
|
||||||
|
minTitleLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Mindest-Titel-Level (optional)'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOfficeRequirement',
|
||||||
|
tableName: 'church_office_requirement',
|
||||||
|
schema: 'falukant_predefine',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOfficeRequirement;
|
||||||
@@ -10,14 +10,14 @@ PromotionalGiftCharacterTrait.init(
|
|||||||
giftId: {
|
giftId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
field: 'gift_id',
|
field: 'gift_id',
|
||||||
references: { model: PromotionalGift, key: 'id' },
|
allowNull: false,
|
||||||
allowNull: false
|
primaryKey: true
|
||||||
},
|
},
|
||||||
traitId: {
|
traitId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
field: 'trait_id',
|
field: 'trait_id',
|
||||||
references: { model: CharacterTrait, key: 'id' },
|
allowNull: false,
|
||||||
allowNull: false
|
primaryKey: true
|
||||||
},
|
},
|
||||||
suitability: {
|
suitability: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
|
|||||||
@@ -10,20 +10,14 @@ PromotionalGiftMood.init(
|
|||||||
giftId: {
|
giftId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
field: 'gift_id',
|
field: 'gift_id',
|
||||||
references: {
|
allowNull: false,
|
||||||
model: PromotionalGift,
|
primaryKey: true
|
||||||
key: 'id'
|
|
||||||
},
|
|
||||||
allowNull: false
|
|
||||||
},
|
},
|
||||||
moodId: {
|
moodId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
field: 'mood_id',
|
field: 'mood_id',
|
||||||
references: {
|
allowNull: false,
|
||||||
model: Mood,
|
primaryKey: true
|
||||||
key: 'id'
|
|
||||||
},
|
|
||||||
allowNull: false
|
|
||||||
},
|
},
|
||||||
suitability: {
|
suitability: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
|
|||||||
38
backend/models/falukant/type/church_office_type.js
Normal file
38
backend/models/falukant/type/church_office_type.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class ChurchOfficeType extends Model {}
|
||||||
|
|
||||||
|
ChurchOfficeType.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
seatsPerRegion: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
regionType: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
hierarchyLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Höhere Zahl = höhere Position in der Hierarchie'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ChurchOfficeType',
|
||||||
|
tableName: 'church_office_type',
|
||||||
|
schema: 'falukant_type',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChurchOfficeType;
|
||||||
@@ -15,7 +15,8 @@ ProductType.init({
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
sellCost: {
|
sellCost: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false}
|
allowNull: false
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'ProductType',
|
modelName: 'ProductType',
|
||||||
|
|||||||
41
backend/models/falukant/type/product_weather_effect.js
Normal file
41
backend/models/falukant/type/product_weather_effect.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
import ProductType from './product.js';
|
||||||
|
import WeatherType from './weather.js';
|
||||||
|
|
||||||
|
class ProductWeatherEffect extends Model {}
|
||||||
|
|
||||||
|
ProductWeatherEffect.init(
|
||||||
|
{
|
||||||
|
productId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
weatherTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
qualityEffect: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Effekt auf Qualität: -2 (sehr negativ), -1 (negativ), 0 (neutral), 1 (positiv), 2 (sehr positiv)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ProductWeatherEffect',
|
||||||
|
tableName: 'product_weather_effect',
|
||||||
|
schema: 'falukant_type',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['product_id', 'weather_type_id']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ProductWeatherEffect;
|
||||||
|
|
||||||
@@ -9,11 +9,7 @@ RegionType.init({
|
|||||||
allowNull: false},
|
allowNull: false},
|
||||||
parentId: {
|
parentId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true,
|
allowNull: true
|
||||||
references: {
|
|
||||||
model: 'region',
|
|
||||||
key: 'id',
|
|
||||||
schema: 'falukant_type'}
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user