mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-02-15 01:02:48 +00:00
Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e8746833 | ||
|
|
0a6e47008e | ||
| 179bf6983c | |||
| 36e9ad6f80 | |||
|
|
72714f58a6 | ||
|
|
29277341b2 | ||
|
|
1908332279 | ||
|
|
231473644d | ||
|
|
6024e63308 | ||
| f4fe8553de | |||
| 794b4dabfa | |||
| 91fac6f7c6 | |||
|
|
93309bf4f3 | ||
|
|
21036c5391 | ||
|
|
d38bd9de65 | ||
| 5d204e0254 | |||
|
|
3cb643754d | ||
|
|
c993b2862b | ||
|
|
5ec508b33c | ||
|
|
b7c09eee7f | ||
|
|
5dfb000f45 | ||
| 761305e333 | |||
| 604de30fa2 | |||
| 07cf1eacc9 | |||
| 9b6d2bbbba | |||
| 0d617c46c6 | |||
|
|
c32ea0f43b | ||
| eaac170a60 | |||
| 7b3c907cf6 | |||
| 9b98dcae9a | |||
| f2e77595b2 | |||
| 04effaa506 | |||
| 75e36abbc6 | |||
| 080923a0e4 | |||
| 901fc01ca6 | |||
|
|
accb62d8ae | ||
|
|
3da25b6519 | ||
| c33ec0bd11 | |||
| 7162f3bd4e | |||
| 66d17db54b | |||
| 1f8f902a28 | |||
| 5d1cabda46 | |||
|
|
f945a4bafc | ||
|
|
b39782b788 | ||
|
|
99bf691ecb | ||
|
|
9814c3e395 | ||
|
|
e0b3b7d375 | ||
|
|
5f5dced68e | ||
|
|
0190f52dfc | ||
|
|
5b3e99bf76 | ||
|
|
b526e1ffb4 | ||
| 8806abda6c | |||
|
|
5294ab963a | ||
|
|
fac44d734e | ||
| 0e4e72429d | |||
| a78c25a258 | |||
| 753a300c3f | |||
|
|
b28cdada6d | ||
|
|
8a04b09fd7 | ||
|
|
b26a06f0ef | ||
|
|
e5231acd4f | ||
|
|
8e8bab6f9d | ||
|
|
71228260e5 | ||
| d96cb32f11 | |||
| bea7c275e8 | |||
| 07ff0a6b5e | |||
| 6fc3c03860 | |||
| 524c4caf5e | |||
| 8136ec1aba | |||
| 0ee476cfff | |||
|
|
b39d1c737b | ||
|
|
dcff378727 | ||
|
|
6c34da94f4 | ||
|
|
f5f6ceb2c5 | ||
|
|
e109ac29b7 | ||
|
|
05aeb1c20d | ||
|
|
003ec2bda0 | ||
|
|
208a6dca01 | ||
| af497deb77 | |||
|
|
ba481547f1 | ||
|
|
9f4c105335 | ||
|
|
8012d56dc8 | ||
|
|
9d994a7554 | ||
|
|
322d2aeb97 | ||
|
|
afee1033af | ||
|
|
d358fdf6b4 | ||
| 150b318aab | |||
| f0f9b7faec | |||
| b22974031c | |||
|
|
0ca45dd06e | ||
| 6a42d12fda | |||
| b7604e9af5 | |||
| 3d00ae4e25 | |||
|
|
fa71b0e189 | ||
|
|
855a049f90 | ||
|
|
679e4876bc | ||
|
|
e84a81bd2f | ||
|
|
34e547553e | ||
|
|
5aa298b3a2 | ||
|
|
626372a741 | ||
|
|
a0ac5157a1 | ||
|
|
083d81be89 | ||
|
|
721fd75288 | ||
|
|
7e6d79f1dc | ||
|
|
bc7e750e83 | ||
|
|
fbe98aa2b4 | ||
|
|
7ae516cf85 | ||
|
|
02bf7f0ab8 | ||
|
|
cc8ab95eaf | ||
|
|
017bfbc820 | ||
|
|
25eeb8d485 | ||
|
|
eca7f5096e | ||
|
|
9f581f4773 | ||
|
|
b767e02263 | ||
|
|
b01d5e5a27 | ||
|
|
06a79225f3 | ||
|
|
eaa9b34d58 | ||
|
|
c4fcd558e3 | ||
|
|
0fef81464f | ||
|
|
9c3d3e2fcd | ||
|
|
cbb57622f7 | ||
|
|
861275aeda | ||
|
|
c0c396653f | ||
|
|
5642e0db2c | ||
|
|
49f7118468 | ||
|
|
9a0c97e6ce | ||
|
|
1dd7fc3d75 | ||
|
|
0e2d90c634 | ||
|
|
56338422a2 | ||
|
|
817ac7226e | ||
|
|
eb81c0b7e4 | ||
|
|
34aa894068 | ||
|
|
d751e7090f | ||
|
|
a9949e1147 | ||
|
|
c78f8deee9 | ||
|
|
2cabe0f4d2 | ||
|
|
0ac7bf908c | ||
|
|
0368676b26 | ||
|
|
cd7931b77b | ||
|
|
801131e351 | ||
|
|
30d30a2f2d | ||
|
|
9dfd5e31ef | ||
|
|
c86d816e56 | ||
|
|
de89810eae | ||
|
|
9df61a752a | ||
|
|
2410fa8527 | ||
|
|
b5b7b5d78c | ||
|
|
00d8fd960d | ||
|
|
caf95664b8 | ||
|
|
66f35ca51d | ||
|
|
c6878f19f9 | ||
|
|
421bfe6755 | ||
| 592a8af42c | |||
|
|
a48510f349 | ||
|
|
a03e5d888b | ||
|
|
7f7c56cec2 | ||
|
|
77d8a5092b | ||
| 60bda5a949 | |||
|
|
ff2b6465db | ||
|
|
72b4af395f | ||
| 2130aa0d68 | |||
|
|
1c7f2f27b5 | ||
|
|
e2ef636cf8 | ||
| 3f69b70820 | |||
|
|
e6244c848c | ||
| 0280ac7201 | |||
| 09327ddc01 | |||
| 49e6f0b7e8 | |||
| c9b5c02862 | |||
| 52459b23f1 | |||
|
|
737f548f38 | ||
|
|
1a29167f08 | ||
|
|
06bd4f1ea4 | ||
|
|
bbb2a189db | ||
|
|
a5f720dba1 | ||
|
|
3b93d255a6 | ||
|
|
5b84b93a16 | ||
|
|
1622c31010 | ||
|
|
09536ed0e8 | ||
|
|
005e2569a1 | ||
|
|
ed328e70d8 | ||
|
|
d96f145e2b | ||
|
|
f3d36bb779 | ||
|
|
5e1d9e280a | ||
|
|
5bb8508df9 | ||
|
|
1b0ef1f81b | ||
|
|
0042914231 | ||
|
|
3f216d84d7 | ||
|
|
88ec6fdef0 | ||
|
|
d22479cfa2 | ||
|
|
ad5108996f | ||
|
|
558e964c83 | ||
|
|
2a7d2f10ed | ||
| b5895afe3e | |||
| 1964ff3eb1 | |||
| 9fc2379090 | |||
| 1e19bd1be7 | |||
|
|
98af683277 | ||
|
|
b40015d408 | ||
|
|
1512167197 | ||
|
|
263f35d2e0 | ||
|
|
05ab27e868 | ||
|
|
f49022e94d | ||
|
|
719457b896 | ||
|
|
6d4fd54c30 | ||
|
|
54aeca0375 | ||
| ffffd9e15f | |||
| b06b29b858 | |||
| ace5e9a89e | |||
| c6fe18f1f7 | |||
| f9af98da97 | |||
| d9ed8a42b5 | |||
| 52da131ee4 | |||
| 6d9529a021 | |||
| 32eca68520 | |||
| e4717e0d89 | |||
| 33ca59343a | |||
|
|
1044231e4e | ||
|
|
2bf39f5ad0 | ||
|
|
68030dc5ec | ||
|
|
ced52e30db | ||
|
|
35c3dd3bbc | ||
|
|
768d80f2a1 | ||
|
|
f7dc901c2a | ||
|
|
fadc37a49c | ||
| 21c253b690 | |||
| 08c8cfe42c | |||
| 59dd435138 | |||
|
|
158e1ee4e0 | ||
|
|
2350e594d4 | ||
| 30560bff69 | |||
|
|
4fc71fc495 | ||
|
|
3ca00c83c0 | ||
|
|
9f91ff1124 | ||
|
|
a15351aa42 | ||
|
|
9a4f7326c1 | ||
|
|
2ed92c0a78 | ||
| de65a380ab | |||
| 45d2283138 | |||
|
|
bdb3c204aa | ||
|
|
8be9662987 | ||
|
|
7ef8340500 | ||
|
|
5d6391f505 | ||
|
|
d46fc1570c | ||
|
|
c752e25221 | ||
|
|
808b3c5a95 | ||
|
|
b05583a8b0 | ||
|
|
90f747a2a9 | ||
|
|
3d85451aff | ||
|
|
8dafc50286 | ||
|
|
0b6360bc0b | ||
|
|
2ad4602de3 | ||
|
|
80eb430460 | ||
|
|
aa772ca19f | ||
|
|
94c0ed83d9 | ||
|
|
ac960d8ecd | ||
|
|
0889e137d9 | ||
|
|
66c049544e | ||
|
|
dc4ce74b21 | ||
|
|
e0d3717ca0 | ||
|
|
73195606fb | ||
|
|
e6fe9ba4b7 | ||
|
|
3c65a2426c | ||
|
|
c9d969afc6 | ||
|
|
573890102f | ||
|
|
64389852c4 | ||
|
|
2536542aa9 | ||
|
|
bcad113682 | ||
|
|
208bfc1550 | ||
|
|
be4d526505 | ||
|
|
9e49afaac3 | ||
|
|
79cbcf2901 | ||
|
|
f83a13c7f2 | ||
|
|
f87d8dacfd | ||
|
|
461a762205 | ||
|
|
9d5cb51ed5 | ||
|
|
e6352cc6fb | ||
|
|
57fe905a96 | ||
|
|
79a8fc5f5f | ||
|
|
3d41d811d2 | ||
|
|
df7b736f5d | ||
|
|
3f4a8a543e | ||
|
|
3fa3d6f5c4 | ||
|
|
4edb8cd541 | ||
|
|
f51890a54f | ||
|
|
b730b91e2c | ||
|
|
2ccaeb6166 | ||
|
|
3a93d9abb3 | ||
| 6f1d94610e | |||
| d70d331e6f | |||
| 396e08127a | |||
| 059072982f | |||
| 8da1d99e33 | |||
| bbbf051026 | |||
| 5e9e55f00e | |||
|
|
3f7a00b8b9 | ||
|
|
e76fed663c | ||
|
|
c8002a2b9a | ||
|
|
b3bde1aa2a | ||
|
|
9b8ac8f1b3 | ||
|
|
baf3134f9d | ||
|
|
19a6badd55 | ||
|
|
bc3e3875e8 | ||
|
|
fe536b626f | ||
|
|
4e43be4ff6 | ||
|
|
da22c5869a | ||
|
|
e77754cb78 | ||
|
|
319f6a90f7 | ||
|
|
562fddb1b4 | ||
|
|
e7c25e15ed | ||
|
|
ce20d5b58e | ||
|
|
de26ce29e6 | ||
|
|
fd83399e70 | ||
|
|
a100c09ac5 | ||
|
|
f6fbe058f6 | ||
|
|
02f34f0fcf | ||
|
|
ce69fe5cb7 | ||
|
|
b941bb24d8 | ||
|
|
58cb1ee096 | ||
|
|
c64d6fbb83 | ||
|
|
702cd07171 | ||
|
|
ba27f81977 | ||
|
|
900dad94ae | ||
|
|
a98e22dba1 | ||
|
|
e9eaafd067 | ||
|
|
21dd977bec | ||
|
|
23cbd42d3f | ||
|
|
201de94230 | ||
|
|
273c0ca1a6 | ||
|
|
4f87a3feec | ||
|
|
dca1ea6c42 | ||
|
|
01ca0a6d8b | ||
|
|
4a574ee430 | ||
|
|
83da96f8c8 | ||
|
|
b7371582dc | ||
|
|
fbc3ee5658 | ||
|
|
4edea10b18 | ||
|
|
0bbaba189f | ||
|
|
a0189658a8 | ||
|
|
f4f16962d9 | ||
|
|
095b1539ad | ||
|
|
6a7256f0c1 | ||
|
|
1a82e6edda | ||
|
|
ffe1ceabf1 | ||
|
|
81bcdc87e3 | ||
|
|
85114b78d6 | ||
|
|
105ab19424 | ||
|
|
7a628d515d | ||
|
|
71258216f2 | ||
|
|
9c35a70e07 | ||
|
|
2a8fef8cb6 | ||
|
|
3a879a954f | ||
|
|
aea2bb079e | ||
|
|
bdeb23a428 | ||
|
|
58b3034fc8 | ||
|
|
d68357c74d | ||
|
|
c05e80420f | ||
|
|
2021531ab4 | ||
|
|
fd15352390 | ||
|
|
86c8c30bd4 | ||
|
|
0d7fe46cc4 | ||
|
|
970613a416 | ||
|
|
2e7acaa68b | ||
|
|
cc456da586 | ||
|
|
47291993d2 | ||
|
|
58f40fbed8 | ||
|
|
3975ccfd71 | ||
|
|
05dce9254c | ||
|
|
85c994e94c | ||
|
|
ea16195296 | ||
|
|
01d0d4819f | ||
|
|
3089f99312 | ||
|
|
c141674c65 | ||
|
|
1a84af06ff | ||
|
|
da25934f4c | ||
|
|
e767ee5870 | ||
|
|
746c48a35f | ||
|
|
8d759834e1 | ||
|
|
f97029dc07 | ||
|
|
2212cf70f5 | ||
|
|
659c8b5106 | ||
|
|
3cdd06b252 |
99
.coderabbit.yaml
Normal file
99
.coderabbit.yaml
Normal file
@ -0,0 +1,99 @@
|
||||
# CodeRabbit Configuration
|
||||
# https://docs.coderabbit.ai/guides/configure-coderabbit
|
||||
|
||||
language: de
|
||||
|
||||
reviews:
|
||||
# Automatisches Review für alle PRs
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
base_branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
auto_approve:
|
||||
enabled: true
|
||||
|
||||
# Review-Einstellungen
|
||||
request_changes_workflow: true
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
changed_files_summary: true
|
||||
|
||||
# Pfad-Filter (Dateien die ignoriert werden)
|
||||
path_filters:
|
||||
- "!**/*.lock"
|
||||
- "!**/package-lock.json"
|
||||
- "!**/yarn.lock"
|
||||
- "!**/*.snap"
|
||||
- "!**/coverage/**"
|
||||
- "!**/dist/**"
|
||||
- "!**/node_modules/**"
|
||||
- "!**/*.min.js"
|
||||
- "!**/*.min.css"
|
||||
- "!**/.git/**"
|
||||
- "!**/storybook-static/**"
|
||||
|
||||
# Instruktionen für spezifische Pfade
|
||||
path_instructions:
|
||||
- path: "webapp/**/*.vue"
|
||||
instructions: |
|
||||
Prüfe Vue.js Best Practices:
|
||||
- Composition API Verwendung
|
||||
- Props Validierung
|
||||
- Event-Handling
|
||||
- Reaktivität
|
||||
|
||||
- path: "webapp/**/*.spec.js"
|
||||
instructions: |
|
||||
Prüfe Test-Qualität:
|
||||
- Aussagekräftige Test-Namen
|
||||
- Edge Cases abgedeckt
|
||||
- Mocking korrekt verwendet
|
||||
|
||||
- path: "backend/**/*.js"
|
||||
instructions: |
|
||||
Prüfe Backend Best Practices:
|
||||
- Error Handling
|
||||
- Input Validierung
|
||||
- SQL Injection Prevention
|
||||
- Performance (N+1 Queries)
|
||||
|
||||
- path: "packages/ui/**/*.ts"
|
||||
instructions: |
|
||||
Prüfe UI Library Standards:
|
||||
- TypeScript Typisierung
|
||||
- Vue 2/3 Kompatibilität (vue-demi)
|
||||
- Accessibility (WCAG 2.1)
|
||||
- CVA Varianten-Pattern
|
||||
|
||||
- path: "packages/ui/**/*.vue"
|
||||
instructions: |
|
||||
Prüfe UI Komponenten:
|
||||
- Render-Funktion Pattern für vue-demi
|
||||
- Props mit korrekten Types
|
||||
- Slots dokumentiert
|
||||
- Keine Template-Syntax (nur h() für Vue 2/3 Kompatibilität)
|
||||
|
||||
# Chat-Befehle
|
||||
chat:
|
||||
auto_reply: true
|
||||
|
||||
# Ton und Stil
|
||||
tone_instructions: |
|
||||
Sei konstruktiv und freundlich.
|
||||
Erkläre das "Warum" hinter Vorschlägen.
|
||||
Priorisiere Sicherheit > Korrektheit > Performance > Lesbarkeit.
|
||||
Schlage konkrete Code-Änderungen vor wenn möglich.
|
||||
|
||||
# Knowledge Base (Repository-spezifisches Wissen)
|
||||
knowledge_base:
|
||||
learnings:
|
||||
scope: auto
|
||||
issues:
|
||||
scope: auto
|
||||
pull_requests:
|
||||
scope: auto
|
||||
68
.github/dependabot.yml
vendored
68
.github/dependabot.yml
vendored
@ -126,3 +126,71 @@ updates:
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
# ui library
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/packages/ui"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
groups:
|
||||
vue:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "vue*"
|
||||
- "@vue*"
|
||||
vite:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "vite*"
|
||||
- "@vitejs*"
|
||||
vitest:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "vitest*"
|
||||
- "@vitest*"
|
||||
|
||||
# ui examples
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/packages/ui/examples/vue3-tailwind"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/packages/ui/examples/vue3-css"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/packages/ui/examples/vue2-tailwind"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
- package-ecosystem: npm
|
||||
open-pull-requests-limit: 99
|
||||
directory: "/packages/ui/examples/vue2-css"
|
||||
rebase-strategy: "disabled"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "saturday"
|
||||
timezone: "Europe/Berlin"
|
||||
time: "03:00"
|
||||
|
||||
6
.github/file-filters.yml
vendored
6
.github/file-filters.yml
vendored
@ -1,5 +1,9 @@
|
||||
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
|
||||
|
||||
ui: &ui
|
||||
- '.github/workflows/ui-*.yml'
|
||||
- 'packages/ui/**/*'
|
||||
|
||||
backend: &backend
|
||||
- '.github/workflows/test-backend.yml'
|
||||
- 'backend/**/*'
|
||||
@ -12,7 +16,9 @@ docker: &docker
|
||||
webapp: &webapp
|
||||
- '.github/workflows/test-webapp.yml'
|
||||
- 'webapp/**/*'
|
||||
- 'styleguide/**/*'
|
||||
- 'package.json'
|
||||
- *ui
|
||||
|
||||
docs-check: &docs-check
|
||||
- '.github/workflows/check-documentation.yml'
|
||||
|
||||
10
.github/workflows/check-documentation.yml
vendored
10
.github/workflows/check-documentation.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
documentation: ${{ steps.changes.outputs.documentation }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Check for markdown file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@ -28,13 +28,13 @@ jobs:
|
||||
if: needs.files-changed.outputs.markdown == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Remove uncheckable documentation files
|
||||
run: rm -rf ./CHANGELOG.md # workaround until https://github.com/gaurav-nelson/github-action-markdown-link-check/pull/183 has been done
|
||||
|
||||
- name: Check Markdown Links
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@1b916f2cf6c36510a6059943104e3c42ce6c16bc # 1.0.15
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@3c3b66f1f7d0900e37b71eca45b63ea9eedfce31 # 1.0.15
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
use-verbose-mode: 'no'
|
||||
@ -51,10 +51,10 @@ jobs:
|
||||
if: needs.files-changed.outputs.documentation == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Setup Node 20
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4.0.3
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
###############################################################################
|
||||
# A Github repo has max 10 GB of cache.
|
||||
# https://github.blog/changelog/2021-11-23-github-actions-cache-size-is-now-increased-to-10gb-per-repository/
|
||||
#
|
||||
# To avoid "cache thrashing" by their cache eviction policy it is recommended
|
||||
# to apply a cache cleanup workflow at PR closing to dele cache leftovers of
|
||||
# the current branch:
|
||||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
###############################################################################
|
||||
|
||||
name: ocelot.social cache cleanup on pr closing
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
clean-branch-cache:
|
||||
name: Cleanup branch cache
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
6
.github/workflows/deploy-documentation.yml
vendored
6
.github/workflows/deploy-documentation.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
documentation: ${{ steps.changes.outputs.documentation }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@ -27,10 +27,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Setup Node 20
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4.0.3
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
18
.github/workflows/docker-push.yml
vendored
18
.github/workflows/docker-push.yml
vendored
@ -24,27 +24,27 @@ jobs:
|
||||
file: backend/Dockerfile
|
||||
target: production
|
||||
- name: webapp-base
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile
|
||||
target: base
|
||||
- name: webapp-build
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile
|
||||
target: build
|
||||
- name: webapp
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile
|
||||
target: production
|
||||
- name: maintenance-base
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
target: base
|
||||
- name: maintenance-build
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
target: build
|
||||
- name: maintenance
|
||||
context: webapp
|
||||
context: .
|
||||
file: webapp/Dockerfile.maintenance
|
||||
target: production
|
||||
runs-on: ubuntu-latest
|
||||
@ -59,16 +59,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
|
||||
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@ -14,9 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full History for changelog
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Setup env
|
||||
run: |
|
||||
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
@ -54,9 +58,13 @@ jobs:
|
||||
needs: [github_tag]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full History for changelog
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Setup env
|
||||
run: |
|
||||
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
@ -64,7 +72,7 @@ jobs:
|
||||
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||
#- name: Repository Dispatch
|
||||
# uses: peter-evans/repository-dispatch@7279ea08e172078316f128ed1118df40d2904f0f # v3.0.0
|
||||
# uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||
# with:
|
||||
# token: ${{ github.token }}
|
||||
# event-type: trigger-ocelot-build-success
|
||||
@ -72,7 +80,7 @@ jobs:
|
||||
# client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
|
||||
|
||||
- name: Repository Dispatch stage.ocelot.social
|
||||
uses: peter-evans/repository-dispatch@7279ea08e172078316f128ed1118df40d2904f0f # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
||||
event-type: trigger-ocelot-build-success
|
||||
@ -80,7 +88,7 @@ jobs:
|
||||
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
|
||||
|
||||
- name: Repository Dispatch stage.yunite.me
|
||||
uses: peter-evans/repository-dispatch@7279ea08e172078316f128ed1118df40d2904f0f # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
||||
event-type: trigger-ocelot-build-success
|
||||
|
||||
40
.github/workflows/test-backend.yml
vendored
40
.github/workflows/test-backend.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
backend: ${{ steps.changes.outputs.backend }}
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Check for backend file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Neo4J | Build 'community' image
|
||||
run: |
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache-neo4j
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/neo4j.tar
|
||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||
@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: backend | Build 'test' image
|
||||
run: |
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache-backend
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/backend.tar
|
||||
key: ${{ github.run_id }}-backend-cache
|
||||
@ -70,7 +70,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version-file: 'backend/.nvmrc'
|
||||
|
||||
- name: backend | Lint
|
||||
run: cd backend && yarn && yarn run lint
|
||||
@ -84,17 +89,17 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Restore Neo4J cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/neo4j.tar
|
||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Restore Backend cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/backend.tar
|
||||
key: ${{ github.run_id }}-backend-cache
|
||||
@ -122,20 +127,3 @@ jobs:
|
||||
- name: backend | Unit test incl. coverage check
|
||||
run: docker compose exec -T backend yarn test
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' }}
|
||||
needs: [files-changed, unit_test_backend]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="${{ github.run_id }}-backend-neo4j-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
KEY="${{ github.run_id }}-backend-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
|
||||
104
.github/workflows/test-e2e.yml
vendored
104
.github/workflows/test-e2e.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||
|
||||
- name: Copy backend env file
|
||||
run: |
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml down
|
||||
|
||||
- name: Cache docker images
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: |
|
||||
/tmp/backend.tar
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||
|
||||
- name: Copy backend env file
|
||||
run: |
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
||||
|
||||
- name: Cache docker image
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-e2e-webapp-cache
|
||||
@ -68,13 +68,16 @@ jobs:
|
||||
name: Fullstack | prepare cypress
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete huge unnecessary tools folder
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.4.0
|
||||
with:
|
||||
node-version-file: 'backend/.tool-versions'
|
||||
node-version-file: 'backend/.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Copy env files
|
||||
@ -84,7 +87,8 @@ jobs:
|
||||
|
||||
- name: Install cypress requirements
|
||||
run: |
|
||||
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
|
||||
sudo wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
|
||||
sudo chmod +x /opt/cucumber-json-formatter
|
||||
cd backend
|
||||
yarn install
|
||||
yarn build
|
||||
@ -93,7 +97,7 @@ jobs:
|
||||
|
||||
- name: Cache docker image
|
||||
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
@ -101,29 +105,45 @@ jobs:
|
||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||
key: ${{ github.run_id }}-e2e-cypress
|
||||
|
||||
fullstack_tests:
|
||||
name: Fullstack | tests
|
||||
if: success()
|
||||
needs: [prepare_backend_environment, prepare_webapp_image, prepare_cypress]
|
||||
list_features:
|
||||
name: List Feature Files
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
jobs: 8
|
||||
strategy:
|
||||
matrix:
|
||||
# run copies of the current job in parallel
|
||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
outputs:
|
||||
features: ${{ steps.list.outputs.features }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||
|
||||
- name: List feature files
|
||||
id: list
|
||||
run: |
|
||||
FEATURES=$(find cypress/e2e/ -maxdepth 1 -name "*.feature" -printf '%f\n' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "features=$FEATURES" >> $GITHUB_OUTPUT
|
||||
|
||||
fullstack_tests:
|
||||
name: E2E | ${{ matrix.feature }}
|
||||
if: success()
|
||||
needs: [prepare_backend_environment, prepare_webapp_image, prepare_cypress, list_features]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
feature: ${{ fromJson(needs.list_features.outputs.features) }}
|
||||
steps:
|
||||
- name: Delete huge unnecessary tools folder
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.4.0
|
||||
with:
|
||||
node-version-file: 'backend/.tool-versions'
|
||||
node-version-file: 'backend/.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Restore cypress cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
@ -133,7 +153,7 @@ jobs:
|
||||
restore-keys: ${{ github.run_id }}-e2e-cypress
|
||||
|
||||
- name: Restore backend environment cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: |
|
||||
/tmp/backend.tar
|
||||
@ -144,7 +164,7 @@ jobs:
|
||||
key: ${{ github.run_id }}-e2e-backend-environment-cache
|
||||
|
||||
- name: Restore webapp cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-e2e-webapp-cache
|
||||
@ -164,7 +184,7 @@ jobs:
|
||||
|
||||
- name: Full stack tests | run tests
|
||||
id: e2e-tests
|
||||
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||
run: yarn run cypress:run --spec "cypress/e2e/${{ matrix.feature }}"
|
||||
|
||||
- name: Full stack tests | if tests failed, compile html report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
@ -175,29 +195,21 @@ jobs:
|
||||
- name: Full stack tests | if tests failed, upload report
|
||||
id: e2e-report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||
name: e2e-report-${{ matrix.feature }}
|
||||
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
|
||||
|
||||
cleanup_cache:
|
||||
name: Cleanup Cache
|
||||
needs: fullstack_tests
|
||||
e2e_status:
|
||||
name: E2E | Status
|
||||
if: always()
|
||||
needs: [fullstack_tests]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
||||
|
||||
- name: Full stack tests | cleanup cache
|
||||
- name: Check E2E results
|
||||
run: |
|
||||
cacheKeys=$(gh cache list --json key --jq '.[] | select(.key | startswith("${{ github.run_id }}-e2e-")) | .key')
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeys
|
||||
do
|
||||
gh cache delete "$cacheKey"
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
if [ "${{ needs.fullstack_tests.result }}" != "success" ]; then
|
||||
echo "E2E tests failed or were cancelled (result: ${{ needs.fullstack_tests.result }})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
50
.github/workflows/test-webapp.yml
vendored
50
.github/workflows/test-webapp.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
webapp: ${{ steps.changes.outputs.webapp }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Check for frontend file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@ -28,7 +28,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version-file: 'webapp/.nvmrc'
|
||||
|
||||
- name: Check translation files
|
||||
run: |
|
||||
@ -42,15 +47,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Webapp | Build 'test' image
|
||||
run: |
|
||||
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
|
||||
docker build --target test -f webapp/Dockerfile -t "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" .
|
||||
docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
||||
|
||||
- name: Cache docker image
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-webapp-cache
|
||||
@ -62,7 +67,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||
with:
|
||||
node-version-file: 'webapp/.nvmrc'
|
||||
|
||||
- name: webapp | Lint
|
||||
run: cd webapp && yarn && yarn run lint
|
||||
@ -76,10 +86,10 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||
|
||||
- name: Restore webapp cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||
with:
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-webapp-cache
|
||||
@ -92,26 +102,8 @@ jobs:
|
||||
cp webapp/.env.template webapp/.env
|
||||
cp backend/.env.template backend/.env
|
||||
|
||||
- name: backend | docker compose
|
||||
# doesn't work without the --build flag - this either means we should not load the cached images or cache the correct image
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp --build
|
||||
- name: Start webapp container
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
|
||||
|
||||
- name: webapp | Unit tests incl. coverage check
|
||||
run: docker compose exec -T webapp yarn test
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' }}
|
||||
needs: [files-changed, unit_test_webapp]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="${{ github.run_id }}-webapp-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
|
||||
|
||||
4
.github/workflows/test.lint_pr.yml
vendored
4
.github/workflows/test.lint_pr.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@e7d011b07ef37e089bea6539210f6a0d360d8af9 # v5.5.3
|
||||
- uses: amannn/action-semantic-pull-request@069817c298f23fab00a8f29a2e556a5eac0f6390 # v5.5.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@ -29,9 +29,11 @@ jobs:
|
||||
# Configure which scopes are allowed (newline delimited).
|
||||
scopes: |
|
||||
backend
|
||||
package/ui
|
||||
webapp
|
||||
maintenance
|
||||
database
|
||||
e2e
|
||||
docu
|
||||
docker
|
||||
release
|
||||
|
||||
95
.github/workflows/ui-build.yml
vendored
Normal file
95
.github/workflows/ui-build.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
name: UI Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build library
|
||||
run: npm run build
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking build output..."
|
||||
|
||||
# Check that dist directory exists
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "::error::dist directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check required files exist
|
||||
FILES=(
|
||||
"dist/index.mjs"
|
||||
"dist/index.cjs"
|
||||
"dist/index.d.ts"
|
||||
"dist/index.d.cts"
|
||||
"dist/tailwind.preset.mjs"
|
||||
"dist/tailwind.preset.cjs"
|
||||
"dist/tailwind.preset.d.ts"
|
||||
"dist/tailwind.preset.d.cts"
|
||||
"dist/style.css"
|
||||
)
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "::error::Missing required file: $file"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ $file"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "All build outputs verified!"
|
||||
|
||||
- name: Validate package
|
||||
run: npm run validate
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: packages/ui/dist/
|
||||
retention-days: 7
|
||||
121
.github/workflows/ui-compatibility.yml
vendored
Normal file
121
.github/workflows/ui-compatibility.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
name: UI Compatibility
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build-library:
|
||||
name: Build Library
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build library
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ui-dist
|
||||
path: packages/ui/dist/
|
||||
retention-days: 1
|
||||
|
||||
test-compatibility:
|
||||
name: Test Compatibility
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: [files-changed, build-library]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example:
|
||||
- vue3-tailwind
|
||||
- vue3-css
|
||||
- vue2-tailwind
|
||||
- vue2-css
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui/examples/${{ matrix.example }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ui-dist
|
||||
path: packages/ui/dist/
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/examples/${{ matrix.example }}/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build example app
|
||||
run: npm run build
|
||||
|
||||
compatibility-result:
|
||||
name: Compatibility Result
|
||||
if: always()
|
||||
needs: [files-changed, test-compatibility]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Skip if no UI changes
|
||||
if: needs.files-changed.outputs.ui != 'true'
|
||||
run: echo "No UI changes detected, skipping."
|
||||
|
||||
- name: Check matrix results
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
run: |
|
||||
if [ "${{ needs.test-compatibility.result }}" != "success" ]; then
|
||||
echo "Compatibility tests failed"
|
||||
exit 1
|
||||
fi
|
||||
59
.github/workflows/ui-docker.yml
vendored
Normal file
59
.github/workflows/ui-docker.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: UI Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build Docker Image
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build development image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./packages/ui
|
||||
file: ./packages/ui/Dockerfile
|
||||
target: development
|
||||
push: false
|
||||
tags: ocelot-social/ui:development
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build production image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./packages/ui
|
||||
file: ./packages/ui/Dockerfile
|
||||
target: production
|
||||
push: false
|
||||
tags: ocelot-social/ui:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
54
.github/workflows/ui-lint.yml
vendored
Normal file
54
.github/workflows/ui-lint.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: UI Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
lint:
|
||||
name: ESLint
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: npm run typecheck
|
||||
65
.github/workflows/ui-release.yml
vendored
Normal file
65
.github/workflows/ui-release.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: UI Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- 'release-please-config.json'
|
||||
- '.release-please-manifest.json'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs['packages/ui--release_created'] }}
|
||||
tag_name: ${{ steps.release.outputs['packages/ui--tag_name'] }}
|
||||
version: ${{ steps.release.outputs['packages/ui--version'] }}
|
||||
|
||||
steps:
|
||||
- name: Release Please
|
||||
id: release
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
config-file: release-please-config.json
|
||||
manifest-file: .release-please-manifest.json
|
||||
|
||||
publish:
|
||||
name: Publish to npm
|
||||
needs: release-please
|
||||
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Validate package
|
||||
run: npm run validate
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
54
.github/workflows/ui-size.yml
vendored
Normal file
54
.github/workflows/ui-size.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: UI Size
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
size:
|
||||
name: Bundle Size Check
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Check bundle size
|
||||
run: npm run size
|
||||
74
.github/workflows/ui-storybook.yml
vendored
Normal file
74
.github/workflows/ui-storybook.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
name: UI Storybook
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build Storybook
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Storybook
|
||||
run: npm run storybook:build
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking Storybook build output..."
|
||||
|
||||
if [ ! -d "storybook-static" ]; then
|
||||
echo "::error::storybook-static directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "storybook-static/index.html" ]; then
|
||||
echo "::error::index.html not found in storybook-static"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Storybook build verified!"
|
||||
|
||||
- name: Upload Storybook artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/ui/storybook-static/
|
||||
retention-days: 7
|
||||
59
.github/workflows/ui-test.yml
vendored
Normal file
59
.github/workflows/ui-test.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: UI Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/ui/coverage/
|
||||
retention-days: 7
|
||||
51
.github/workflows/ui-verify.yml
vendored
Normal file
51
.github/workflows/ui-verify.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: UI Verify
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
verify:
|
||||
name: Completeness Check
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check component completeness
|
||||
run: npm run verify
|
||||
64
.github/workflows/ui-visual.yml
vendored
Normal file
64
.github/workflows/ui-visual.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
name: UI Visual
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
visual:
|
||||
name: Visual Regression
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'packages/ui/.tool-versions'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: packages/ui/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Run visual tests
|
||||
run: npm run test:visual
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v6
|
||||
if: failure()
|
||||
with:
|
||||
name: visual-test-results
|
||||
path: |
|
||||
packages/ui/test-results/
|
||||
packages/ui/playwright-report/
|
||||
retention-days: 7
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,6 +1,3 @@
|
||||
[submodule "styleguide"]
|
||||
path = styleguide
|
||||
url = https://github.com/Human-Connection/Nitro-Styleguide.git
|
||||
[submodule "deployment/configurations/stage.ocelot.social"]
|
||||
path = deployment/configurations/stage.ocelot.social
|
||||
url = git@github.com:Ocelot-Social-Community/stage.ocelot.social.git
|
||||
|
||||
3
.release-please-manifest.json
Normal file
3
.release-please-manifest.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"packages/ui": "0.0.1"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
nodejs 20.12.1
|
||||
2674
CHANGELOG.md
2674
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,7 @@ SMTP_PASSWORD=
|
||||
SMTP_SECURE="false" # true for 465, false for other ports
|
||||
SMTP_DKIM_DOMAINNAME=
|
||||
SMTP_DKIM_KEYSELECTOR=
|
||||
SMTP_DKIM_PRIVATKEY=
|
||||
SMTP_DKIM_PRIVATEKEY=
|
||||
# E-Mail settings for our 'docker compose up mailserver'
|
||||
# SMTP_HOST=localhost
|
||||
# SMTP_PORT=1025
|
||||
@ -48,3 +48,4 @@ IMAGOR_SECRET=mysecret
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
MAX_PINNED_POSTS=1
|
||||
MAX_GROUP_PINNED_POSTS=1
|
||||
|
||||
@ -19,7 +19,7 @@ SMTP_PASSWORD=
|
||||
SMTP_SECURE="false" # true for 465, false for other ports
|
||||
SMTP_DKIM_DOMAINNAME=
|
||||
SMTP_DKIM_KEYSELECTOR=
|
||||
SMTP_DKIM_PRIVATKEY=
|
||||
SMTP_DKIM_PRIVATEKEY=
|
||||
|
||||
JWT_SECRET="b/&&7b78BF&fv/Vd"
|
||||
JWT_EXPIRES="2y"
|
||||
@ -40,3 +40,4 @@ IMAGOR_SECRET=mysecret
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
MAX_PINNED_POSTS=1
|
||||
MAX_GROUP_PINNED_POSTS=1
|
||||
|
||||
@ -14,7 +14,6 @@ module.exports = {
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:security/recommended-legacy',
|
||||
'plugin:@eslint-community/eslint-comments/recommended',
|
||||
'prettier',
|
||||
],
|
||||
@ -175,6 +174,10 @@ module.exports = {
|
||||
'@eslint-community/eslint-comments/require-description': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.cjs', '*.ts', '*.tsx'],
|
||||
extends: ['plugin:security/recommended-legacy'],
|
||||
},
|
||||
// only for ts files
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
@ -228,5 +231,33 @@ module.exports = {
|
||||
files: ['*.json', '*.json5', '*.jsonc'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
},
|
||||
{
|
||||
files: ['*.graphql', '*.gql'],
|
||||
parser: '@graphql-eslint/eslint-plugin',
|
||||
plugins: ['@graphql-eslint'],
|
||||
extends: ['plugin:@graphql-eslint/schema-recommended'],
|
||||
rules: {
|
||||
'@graphql-eslint/description-style': ['error', { style: 'inline' }],
|
||||
'@graphql-eslint/require-description': 'off',
|
||||
'@graphql-eslint/naming-convention': 'off',
|
||||
'@graphql-eslint/strict-id-in-types': 'off',
|
||||
'@graphql-eslint/no-typename-prefix': 'off',
|
||||
// incompatible: `depends on a GraphQL validation rule "XXX" but it's not available in the "graphql" version you are using. Skipping…`
|
||||
'@graphql-eslint/known-directives': 'off',
|
||||
'@graphql-eslint/known-argument-names': 'off',
|
||||
'@graphql-eslint/known-type-names': 'off',
|
||||
'@graphql-eslint/lone-schema-definition': 'off',
|
||||
'@graphql-eslint/provided-required-arguments': 'off',
|
||||
'@graphql-eslint/unique-directive-names': 'off',
|
||||
'@graphql-eslint/unique-directive-names-per-location': 'off',
|
||||
'@graphql-eslint/unique-field-definition-names': 'off',
|
||||
'@graphql-eslint/unique-operation-types': 'off',
|
||||
'@graphql-eslint/unique-type-names': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
schema: './src/graphql/types/**/*.gql',
|
||||
assumeValid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
v24.2.0
|
||||
v25.3.0
|
||||
|
||||
@ -1 +0,0 @@
|
||||
nodejs 24.2.0
|
||||
@ -1,4 +1,5 @@
|
||||
FROM node:24.8.0-alpine AS base
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:25.6.0-alpine AS base
|
||||
LABEL org.label-schema.name="ocelot.social:backend"
|
||||
LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
|
||||
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
|
||||
@ -28,13 +29,15 @@ ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
|
||||
ONBUILD COPY ./branding/middlewares/ src/middleware/branding/
|
||||
ONBUILD COPY ./branding/data/ src/db/data
|
||||
ONBUILD COPY ./branding/public/ public/
|
||||
ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
ONBUILD RUN --mount=type=cache,target=/yarn-cache,sharing=locked \
|
||||
yarn install --production=false --frozen-lockfile --non-interactive --cache-folder /yarn-cache
|
||||
ONBUILD RUN yarn run build
|
||||
ONBUILD RUN mkdir /build
|
||||
ONBUILD RUN cp -r ./build /build
|
||||
ONBUILD RUN cp -r ./public /build
|
||||
ONBUILD RUN cp -r ./package.json yarn.lock /build
|
||||
ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive
|
||||
ONBUILD RUN --mount=type=cache,target=/yarn-cache,sharing=locked \
|
||||
cd /build && yarn install --production=true --frozen-lockfile --non-interactive --cache-folder /yarn-cache
|
||||
|
||||
FROM build AS test
|
||||
# required for the migrations
|
||||
|
||||
@ -19,18 +19,16 @@ Wait a little until your backend is up and running at [http://localhost:4000/](h
|
||||
## Installation without Docker
|
||||
|
||||
For the local installation you need a recent version of
|
||||
[Node](https://nodejs.org/en/) (>= `v16.19.0`). We are using
|
||||
`v24.2.0` and therefore we recommend to use the same version
|
||||
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
|
||||
some known problems with more recent node versions). You can use the
|
||||
[Node](https://nodejs.org/en/). We are using
|
||||
`v25.3.0` and therefore we recommend to use the same version. You can use the
|
||||
[node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||
between different local Node versions:
|
||||
|
||||
```sh
|
||||
# install Node
|
||||
# install Node using '.nvmrc' file
|
||||
$ cd backend
|
||||
$ nvm install v24.2.0
|
||||
$ nvm use v24.2.0
|
||||
$ nvm install
|
||||
$ nvm use
|
||||
```
|
||||
|
||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||
|
||||
@ -18,7 +18,7 @@ module.exports = {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 92,
|
||||
lines: 93,
|
||||
},
|
||||
},
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social-backend",
|
||||
"version": "3.12.2",
|
||||
"version": "3.14.1",
|
||||
"description": "GraphQL Backend for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
@ -12,7 +12,7 @@
|
||||
"build": "tsc && tsc-alias && ./scripts/build.copy.files.sh",
|
||||
"dev": "nodemon --exec ts-node --require tsconfig-paths/register src/index.ts -e js,ts,gql",
|
||||
"dev:debug": "nodemon --exec node --inspect=0.0.0.0:9229 build/src/index.js -e js,ts,gql",
|
||||
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc .",
|
||||
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc,.graphql,.gql .",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
|
||||
"db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts",
|
||||
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
|
||||
@ -23,26 +23,29 @@
|
||||
"db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts",
|
||||
"db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts",
|
||||
"db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create",
|
||||
"db:func:disable:notifications": "ts-node --require tsconfig-paths/register src/db/disable-notifications.ts",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
|
||||
"prod:db:data:branding": "node build/src/db/data-branding.js",
|
||||
"prod:db:data:categories": "node build/src/db/categories.js"
|
||||
"prod:db:data:categories": "node build/src/db/categories.js",
|
||||
"prod:db:data:admin": "node build/src/db/admin.js",
|
||||
"prod:db:func:disable:notifications": "node build/src/db/disable-notifications.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.888.0",
|
||||
"@aws-sdk/lib-storage": "^3.888.0",
|
||||
"@sentry/node": "^5.15.4",
|
||||
"@aws-sdk/client-s3": "^3.990.0",
|
||||
"@aws-sdk/lib-storage": "^3.985.0",
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"apollo-server": "~2.14.2",
|
||||
"apollo-server-express": "^2.14.2",
|
||||
"bcryptjs": "~3.0.2",
|
||||
"bcryptjs": "~3.0.3",
|
||||
"body-parser": "^1.20.3",
|
||||
"cheerio": "~1.1.2",
|
||||
"cross-env": "~10.0.0",
|
||||
"cheerio": "~1.2.0",
|
||||
"cross-env": "~10.1.0",
|
||||
"dotenv": "~17.0.1",
|
||||
"email-templates": "^12.0.3",
|
||||
"express": "^5.1.0",
|
||||
"email-templates": "^13.0.1",
|
||||
"express": "^4.22.1",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-middleware": "~4.0.2",
|
||||
"graphql-middleware": "~6.1.35",
|
||||
"graphql-middleware-sentry": "^3.2.1",
|
||||
"graphql-redis-subscriptions": "^2.7.0",
|
||||
"graphql-shield": "~7.2.2",
|
||||
@ -50,55 +53,56 @@
|
||||
"graphql-tag": "~2.10.3",
|
||||
"graphql-upload": "^13.0.0",
|
||||
"helmet": "~8.1.0",
|
||||
"ioredis": "^5.7.0",
|
||||
"ioredis": "^5.9.2",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"languagedetect": "^2.0.0",
|
||||
"linkify-html": "^4.3.2",
|
||||
"linkifyjs": "^4.3.2",
|
||||
"lodash": "~4.17.21",
|
||||
"lodash": "~4.17.23",
|
||||
"merge-graphql-schemas": "^1.7.8",
|
||||
"metascraper": "^5.49.2",
|
||||
"metascraper-author": "^5.49.2",
|
||||
"metascraper-date": "^5.49.2",
|
||||
"metascraper-description": "^5.49.2",
|
||||
"metascraper-image": "^5.49.2",
|
||||
"metascraper-lang": "^5.49.2",
|
||||
"metascraper": "^5.49.19",
|
||||
"metascraper-author": "^5.49.19",
|
||||
"metascraper-date": "^5.49.19",
|
||||
"metascraper-description": "^5.49.19",
|
||||
"metascraper-image": "^5.49.19",
|
||||
"metascraper-lang": "^5.49.19",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.49.2",
|
||||
"metascraper-publisher": "^5.49.2",
|
||||
"metascraper-logo": "^5.49.19",
|
||||
"metascraper-publisher": "^5.49.19",
|
||||
"metascraper-soundcloud": "^5.34.4",
|
||||
"metascraper-title": "^5.49.2",
|
||||
"metascraper-url": "^5.49.2",
|
||||
"metascraper-video": "^5.49.2",
|
||||
"metascraper-youtube": "^5.49.2",
|
||||
"metascraper-title": "^5.49.19",
|
||||
"metascraper-url": "^5.49.19",
|
||||
"metascraper-video": "^5.49.19",
|
||||
"metascraper-youtube": "^5.49.20",
|
||||
"migrate": "^2.1.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"mime-types": "^3.0.2",
|
||||
"minimatch": "^10.1.2",
|
||||
"mustache": "^4.2.0",
|
||||
"neo4j-driver": "^4.4.11",
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"neo4j-graphql-js": "2.11.5",
|
||||
"neode": "^0.4.9",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^7.0.6",
|
||||
"nodemailer": "^8.0.1",
|
||||
"nodemailer-html-to-text": "^3.2.0",
|
||||
"preview-email": "^3.1.0",
|
||||
"preview-email": "^3.1.1",
|
||||
"pug": "^3.0.3",
|
||||
"sanitize-html": "~2.17.0",
|
||||
"slugify": "^1.6.6",
|
||||
"trunc-html": "~1.1.2",
|
||||
"tslog": "^4.9.3",
|
||||
"tslog": "^4.10.2",
|
||||
"uuid": "~9.0.1",
|
||||
"validator": "^13.15.15",
|
||||
"validator": "^13.15.26",
|
||||
"xregexp": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
||||
"@faker-js/faker": "9.9.0",
|
||||
"@graphql-eslint/eslint-plugin": "^3.20.1",
|
||||
"@types/email-templates": "^10.0.4",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jsonwebtoken": "~8.5.1",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.4.0",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/request": "^2.48.13",
|
||||
"@types/slug": "^5.0.9",
|
||||
"@types/uuid": "~9.0.1",
|
||||
@ -110,19 +114,19 @@
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-jsonc": "^2.20.1",
|
||||
"eslint-plugin-n": "^17.21.3",
|
||||
"eslint-plugin-jest": "^29.13.0",
|
||||
"eslint-plugin-jsonc": "^2.21.1",
|
||||
"eslint-plugin-n": "^17.23.2",
|
||||
"eslint-plugin-no-catch-all": "^1.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"jest": "^30.1.3",
|
||||
"nodemon": "~3.1.10",
|
||||
"prettier": "^3.6.2",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "~3.1.11",
|
||||
"prettier": "^3.8.1",
|
||||
"require-json5": "^1.3.0",
|
||||
"rosie": "^2.1.1",
|
||||
"ts-jest": "^29.4.1",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
@ -133,7 +137,9 @@
|
||||
"**/graphql-upload": "^11.0.0",
|
||||
"**/strip-ansi": "6.0.1",
|
||||
"**/string-width": "4.2.0",
|
||||
"**/wrap-ansi": "7.0.0"
|
||||
"**/wrap-ansi": "7.0.0",
|
||||
"**/jwa": "^2.0.1",
|
||||
"**/@types/express": "4.17.25"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.12.1"
|
||||
|
||||
@ -48,7 +48,7 @@ const SMTP_PASSWORD = env.SMTP_PASSWORD
|
||||
const SMTP_DKIM_DOMAINNAME = env.SMTP_DKIM_DOMAINNAME
|
||||
const SMTP_DKIM_KEYSELECTOR = env.SMTP_DKIM_KEYSELECTOR
|
||||
// PEM format = https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html
|
||||
const SMTP_DKIM_PRIVATKEY = env.SMTP_DKIM_PRIVATKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break
|
||||
const SMTP_DKIM_PRIVATEKEY = env.SMTP_DKIM_PRIVATEKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break
|
||||
const SMTP_MAX_CONNECTIONS = (env.SMTP_MAX_CONNECTIONS && parseInt(env.SMTP_MAX_CONNECTIONS)) || 5
|
||||
const SMTP_MAX_MESSAGES = (env.SMTP_MAX_MESSAGES && parseInt(env.SMTP_MAX_MESSAGES)) || 100
|
||||
|
||||
@ -67,11 +67,11 @@ if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||
pass: SMTP_PASSWORD,
|
||||
}
|
||||
}
|
||||
if (SMTP_DKIM_DOMAINNAME && SMTP_DKIM_KEYSELECTOR && SMTP_DKIM_PRIVATKEY) {
|
||||
if (SMTP_DKIM_DOMAINNAME && SMTP_DKIM_KEYSELECTOR && SMTP_DKIM_PRIVATEKEY) {
|
||||
nodemailerTransportOptions.dkim = {
|
||||
domainName: SMTP_DKIM_DOMAINNAME,
|
||||
keySelector: SMTP_DKIM_KEYSELECTOR,
|
||||
privateKey: SMTP_DKIM_PRIVATKEY,
|
||||
privateKey: SMTP_DKIM_PRIVATEKEY,
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,6 +138,9 @@ const options = {
|
||||
MAX_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_PINNED_POSTS))
|
||||
? 1
|
||||
: Number(process.env.MAX_PINNED_POSTS),
|
||||
MAX_GROUP_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_GROUP_PINNED_POSTS))
|
||||
? 1
|
||||
: Number(process.env.MAX_GROUP_PINNED_POSTS),
|
||||
}
|
||||
|
||||
const language = {
|
||||
|
||||
61
backend/src/db/disable-notifications.ts
Normal file
61
backend/src/db/disable-notifications.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import databaseContext from '@context/database'
|
||||
|
||||
const run = async () => {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length !== 1) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Usage: yarn run db:func:disable-notifications <email>')
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const email = args[0]
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(email)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error: Invalid email address format')
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { write } = databaseContext()
|
||||
|
||||
const result = (
|
||||
await write({
|
||||
query: `
|
||||
MATCH (:EmailAddress {email: $email})-[:BELONGS_TO]->(user:User)
|
||||
SET user.emailNotificationsFollowingUsers = false
|
||||
SET user.emailNotificationsPostInGroup = false
|
||||
SET user.emailNotificationsCommentOnObservedPost = false
|
||||
SET user.emailNotificationsMention = false
|
||||
SET user.emailNotificationsChatMessage = false
|
||||
SET user.emailNotificationsGroupMemberJoined = false
|
||||
SET user.emailNotificationsGroupMemberLeft = false
|
||||
SET user.emailNotificationsGroupMemberRemoved = false
|
||||
SET user.emailNotificationsGroupMemberRoleChanged = false
|
||||
RETURN toString(count(user)) as count
|
||||
`,
|
||||
variables: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
).records[0].get('count') as string
|
||||
|
||||
if (result !== '1') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`User with email address ${email} not found`)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Notifications for User with email address ${email} disabled`)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
void (async function () {
|
||||
await run()
|
||||
})()
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
},
|
||||
title: { type: 'string', disallow: [null], min: 3 },
|
||||
slug: { type: 'string', allow: [null], unique: 'true' },
|
||||
content: { type: 'string', disallow: [null], min: 3 },
|
||||
content: { type: 'string', disallow: [null], required: true, min: 3 },
|
||||
contentExcerpt: { type: 'string', allow: [null] },
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
@ -58,6 +58,7 @@ export default {
|
||||
},
|
||||
},
|
||||
pinned: { type: 'boolean', default: null, valid: [null, true] },
|
||||
groupPinned: { type: 'boolean', default: null, valid: [null, true] },
|
||||
postType: { type: 'string', default: 'Article', valid: ['Article', 'Event'] },
|
||||
observes: {
|
||||
type: 'relationship',
|
||||
|
||||
@ -1,36 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable n/no-missing-require */
|
||||
/* eslint-disable n/global-require */
|
||||
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare let Cypress: any | undefined
|
||||
//
|
||||
// We use static imports instead of dynamic require() to ensure compatibility
|
||||
// with both Node.js and Webpack (used by Cypress cucumber preprocessor).
|
||||
|
||||
import Badge from './Badge'
|
||||
import Category from './Category'
|
||||
import Comment from './Comment'
|
||||
import Donations from './Donations'
|
||||
import EmailAddress from './EmailAddress'
|
||||
import File from './File'
|
||||
import Group from './Group'
|
||||
import Image from './Image'
|
||||
import InviteCode from './InviteCode'
|
||||
import Location from './Location'
|
||||
import Migration from './Migration'
|
||||
import Post from './Post'
|
||||
import Report from './Report'
|
||||
import SocialMedia from './SocialMedia'
|
||||
import Tag from './Tag'
|
||||
import UnverifiedEmailAddress from './UnverifiedEmailAddress'
|
||||
import User from './User'
|
||||
|
||||
import type Neode from 'neode'
|
||||
|
||||
// Type assertion needed because TypeScript infers literal types from the model
|
||||
// objects (e.g., type: 'string' as literal), but Neode expects the broader
|
||||
// SchemaObject type with PropertyTypes union. The Neode type definitions are
|
||||
// incomplete/incorrect, so we use double assertion to bypass the check.
|
||||
export default {
|
||||
File: typeof Cypress !== 'undefined' ? require('./File') : require('./File').default,
|
||||
Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default,
|
||||
Badge: typeof Cypress !== 'undefined' ? require('./Badge') : require('./Badge').default,
|
||||
User: typeof Cypress !== 'undefined' ? require('./User') : require('./User').default,
|
||||
Group: typeof Cypress !== 'undefined' ? require('./Group') : require('./Group').default,
|
||||
EmailAddress:
|
||||
typeof Cypress !== 'undefined' ? require('./EmailAddress') : require('./EmailAddress').default,
|
||||
UnverifiedEmailAddress:
|
||||
typeof Cypress !== 'undefined'
|
||||
? require('./UnverifiedEmailAddress')
|
||||
: require('./UnverifiedEmailAddress').default,
|
||||
SocialMedia:
|
||||
typeof Cypress !== 'undefined' ? require('./SocialMedia') : require('./SocialMedia').default,
|
||||
Post: typeof Cypress !== 'undefined' ? require('./Post') : require('./Post').default,
|
||||
Comment: typeof Cypress !== 'undefined' ? require('./Comment') : require('./Comment').default,
|
||||
Category: typeof Cypress !== 'undefined' ? require('./Category') : require('./Category').default,
|
||||
Tag: typeof Cypress !== 'undefined' ? require('./Tag') : require('./Tag').default,
|
||||
Location: typeof Cypress !== 'undefined' ? require('./Location') : require('./Location').default,
|
||||
Donations:
|
||||
typeof Cypress !== 'undefined' ? require('./Donations') : require('./Donations').default,
|
||||
Report: typeof Cypress !== 'undefined' ? require('./Report') : require('./Report').default,
|
||||
Migration:
|
||||
typeof Cypress !== 'undefined' ? require('./Migration') : require('./Migration').default,
|
||||
InviteCode:
|
||||
typeof Cypress !== 'undefined' ? require('./InviteCode') : require('./InviteCode').default,
|
||||
}
|
||||
Badge,
|
||||
Category,
|
||||
Comment,
|
||||
Donations,
|
||||
EmailAddress,
|
||||
File,
|
||||
Group,
|
||||
Image,
|
||||
InviteCode,
|
||||
Location,
|
||||
Migration,
|
||||
Post,
|
||||
Report,
|
||||
SocialMedia,
|
||||
Tag,
|
||||
UnverifiedEmailAddress,
|
||||
User,
|
||||
} as unknown as Record<string, Neode.SchemaObject>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import path from 'node:path'
|
||||
|
||||
import Email from 'email-templates'
|
||||
@ -94,8 +93,8 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
: notification?.from?.title,
|
||||
postUrl: new URL(
|
||||
notification?.from?.__typename === 'Comment'
|
||||
? `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}`
|
||||
: `/post/${notification?.from?.id}/${notification?.from?.slug}`,
|
||||
? `/post/${encodeURIComponent(notification?.from?.post?.id)}/${encodeURIComponent(notification?.from?.post?.slug)}`
|
||||
: `/post/${encodeURIComponent(notification?.from?.id)}/${encodeURIComponent(notification?.from?.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
),
|
||||
postAuthorName:
|
||||
@ -106,7 +105,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
notification?.from?.__typename === 'Comment'
|
||||
? undefined
|
||||
: new URL(
|
||||
`profile/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
|
||||
`profile/${encodeURIComponent(notification?.from?.author?.id)}/${encodeURIComponent(notification?.from?.author?.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
),
|
||||
commenterName:
|
||||
@ -116,14 +115,14 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
commenterUrl:
|
||||
notification?.from?.__typename === 'Comment'
|
||||
? new URL(
|
||||
`/profile/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
|
||||
`/profile/${encodeURIComponent(notification?.from?.author?.id)}/${encodeURIComponent(notification?.from?.author?.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
commentUrl:
|
||||
notification?.from?.__typename === 'Comment'
|
||||
? new URL(
|
||||
`/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}#commentId-${notification?.from?.id}`,
|
||||
`/post/${encodeURIComponent(notification?.from?.post?.id)}/${encodeURIComponent(notification?.from?.post?.slug)}#commentId-${encodeURIComponent(notification?.from?.id)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -132,7 +131,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
groupUrl:
|
||||
notification?.from?.__typename === 'Group'
|
||||
? new URL(
|
||||
`/groups/${notification?.from?.id}/${notification?.from?.slug}`,
|
||||
`/groups/${encodeURIComponent(notification?.from?.id)}/${encodeURIComponent(notification?.from?.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -143,7 +142,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
groupRelatedUserUrl:
|
||||
notification?.from?.__typename === 'Group'
|
||||
? new URL(
|
||||
`/profile/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
|
||||
`/profile/${encodeURIComponent(notification?.relatedUser?.id)}/${encodeURIComponent(notification?.relatedUser?.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -177,7 +176,10 @@ export const sendChatMessageMail = async (
|
||||
locale: recipientUser.locale,
|
||||
name: recipientUser.name,
|
||||
chattingUser: senderUser.name,
|
||||
chattingUserUrl: new URL(`/profile/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI),
|
||||
chattingUserUrl: new URL(
|
||||
`/profile/${encodeURIComponent(senderUser.id)}/${encodeURIComponent(senderUser.slug)}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
),
|
||||
chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
||||
export const ChangeGroupMemberRole = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
||||
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
user {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
membership {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -8,6 +8,8 @@ export const CreateComment = gql`
|
||||
author {
|
||||
name
|
||||
}
|
||||
isPostObservedByMe
|
||||
postObservingUsersCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -45,6 +45,7 @@ export const CreatePost = gql`
|
||||
}
|
||||
isObservedByMe
|
||||
observingUsersCount
|
||||
language
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
14
backend/src/graphql/queries/CreateSocialMedia.ts
Normal file
14
backend/src/graphql/queries/CreateSocialMedia.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const CreateSocialMedia = gql`
|
||||
mutation ($url: String!) {
|
||||
CreateSocialMedia(url: $url) {
|
||||
id
|
||||
url
|
||||
url
|
||||
ownedBy {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
||||
export const GroupMembers = gql`
|
||||
query GroupMembers($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
user {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
membership {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
||||
export const JoinGroup = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
JoinGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
user {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
membership {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
||||
export const LeaveGroup = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
LeaveGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
user {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
membership {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -6,10 +6,34 @@ export const Post = gql`
|
||||
id
|
||||
title
|
||||
content
|
||||
contentExcerpt
|
||||
eventStart
|
||||
pinned
|
||||
createdAt
|
||||
pinnedAt
|
||||
isObservedByMe
|
||||
observingUsersCount
|
||||
clickedCount
|
||||
emotionsCount
|
||||
emotions {
|
||||
emotion
|
||||
User {
|
||||
id
|
||||
}
|
||||
}
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
shoutedBy {
|
||||
id
|
||||
}
|
||||
tags {
|
||||
id
|
||||
}
|
||||
comments {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
||||
export const RemoveUserFromGroup = gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
RemoveUserFromGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
user {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
membership {
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -8,6 +8,8 @@ export const SignupVerification = gql`
|
||||
$slug: String
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
$about: String
|
||||
$locale: String
|
||||
) {
|
||||
SignupVerification(
|
||||
email: $email
|
||||
@ -16,9 +18,13 @@ export const SignupVerification = gql`
|
||||
slug: $slug
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
about: $about
|
||||
locale: $locale
|
||||
) {
|
||||
id
|
||||
slug
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,10 +1,44 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const UpdatePost = gql`
|
||||
mutation ($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
|
||||
mutation (
|
||||
$id: ID!
|
||||
$title: String!
|
||||
$content: String!
|
||||
$image: ImageInput
|
||||
$categoryIds: [ID]
|
||||
$postType: PostType
|
||||
$eventInput: _EventInput
|
||||
) {
|
||||
UpdatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
image: $image
|
||||
categoryIds: $categoryIds
|
||||
postType: $postType
|
||||
eventInput: $eventInput
|
||||
) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
categories {
|
||||
id
|
||||
}
|
||||
postType
|
||||
eventStart
|
||||
eventLocationName
|
||||
eventVenue
|
||||
eventLocation {
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,9 +1,38 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const UpdateUser = gql`
|
||||
mutation ($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
mutation (
|
||||
$id: ID!
|
||||
$name: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$locationName: String # empty string '' sets it to null
|
||||
$emailNotificationSettings: [EmailNotificationSettingsInput]
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
name: $name
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
emailNotificationSettings: $emailNotificationSettings
|
||||
) {
|
||||
id
|
||||
name
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
nameRU
|
||||
}
|
||||
emailNotificationSettings {
|
||||
type
|
||||
settings {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,9 +1,165 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const User = gql`
|
||||
query ($name: String) {
|
||||
User(name: $name) {
|
||||
email
|
||||
query ($id: ID, $name: String, $email: String) {
|
||||
User(id: $id, name: $name, email: $email) {
|
||||
id
|
||||
name
|
||||
badgeTrophiesCount
|
||||
badgeTrophies {
|
||||
id
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
followedBy {
|
||||
id
|
||||
}
|
||||
followedByCurrentUser
|
||||
following {
|
||||
name
|
||||
slug
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
comments {
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
image {
|
||||
url
|
||||
}
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
}
|
||||
isMuted
|
||||
isBlocked
|
||||
location {
|
||||
distanceToMe
|
||||
}
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UserEmailNotificationSettings = gql`
|
||||
query ($id: ID, $name: String, $email: String) {
|
||||
User(id: $id, name: $name, email: $email) {
|
||||
id
|
||||
name
|
||||
badgeTrophiesCount
|
||||
badgeTrophies {
|
||||
id
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
followedBy {
|
||||
id
|
||||
}
|
||||
followedByCurrentUser
|
||||
following {
|
||||
name
|
||||
slug
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
comments {
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
image {
|
||||
url
|
||||
}
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
}
|
||||
isMuted
|
||||
isBlocked
|
||||
location {
|
||||
distanceToMe
|
||||
}
|
||||
emailNotificationSettings {
|
||||
type
|
||||
settings {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UserEmail = gql`
|
||||
query ($id: ID, $name: String, $email: String) {
|
||||
User(id: $id, name: $name, email: $email) {
|
||||
id
|
||||
name
|
||||
email
|
||||
badgeTrophiesCount
|
||||
badgeTrophies {
|
||||
id
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
followedBy {
|
||||
id
|
||||
}
|
||||
followedByCurrentUser
|
||||
following {
|
||||
name
|
||||
slug
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
comments {
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
image {
|
||||
url
|
||||
}
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
}
|
||||
isMuted
|
||||
isBlocked
|
||||
location {
|
||||
distanceToMe
|
||||
}
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
7
backend/src/graphql/queries/availableRoles.ts
Normal file
7
backend/src/graphql/queries/availableRoles.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const availableRoles = gql`
|
||||
query {
|
||||
availableRoles
|
||||
}
|
||||
`
|
||||
@ -3,6 +3,15 @@ import gql from 'graphql-tag'
|
||||
export const currentUser = gql`
|
||||
query currentUser {
|
||||
currentUser {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
email
|
||||
role
|
||||
activeCategories
|
||||
following {
|
||||
name
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import gql from 'graphql-tag'
|
||||
export const followUser = gql`
|
||||
mutation ($id: ID!) {
|
||||
followUser(id: $id) {
|
||||
id
|
||||
name
|
||||
followedBy {
|
||||
id
|
||||
|
||||
@ -3,6 +3,7 @@ import gql from 'graphql-tag'
|
||||
export const markAllAsRead = gql`
|
||||
mutation {
|
||||
markAllAsRead {
|
||||
id
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
|
||||
@ -3,14 +3,23 @@ import gql from 'graphql-tag'
|
||||
export const notifications = gql`
|
||||
query ($read: Boolean, $orderBy: NotificationOrdering) {
|
||||
notifications(read: $read, orderBy: $orderBy) {
|
||||
reason
|
||||
relatedUser {
|
||||
id
|
||||
}
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
id
|
||||
content
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
content
|
||||
}
|
||||
... on Group {
|
||||
id
|
||||
}
|
||||
}
|
||||
read
|
||||
createdAt
|
||||
|
||||
25
backend/src/graphql/queries/pinGroupPost.ts
Normal file
25
backend/src/graphql/queries/pinGroupPost.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const pinGroupPost = gql`
|
||||
mutation ($id: ID!) {
|
||||
pinGroupPost(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinnedAt
|
||||
pinned
|
||||
groupPinned
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -11,6 +11,7 @@ export const profilePagePosts = gql`
|
||||
id
|
||||
title
|
||||
content
|
||||
groupPinned
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const reports = gql`
|
||||
query ($closed: Boolean) {
|
||||
reports(orderBy: createdAt_desc, closed: $closed) {
|
||||
query (
|
||||
$orderBy: ReportOrdering
|
||||
$reviewed: Boolean
|
||||
$closed: Boolean
|
||||
$first: Int
|
||||
$offset: Int
|
||||
) {
|
||||
reports(
|
||||
orderBy: $orderBy
|
||||
reviewed: $reviewed
|
||||
closed: $closed
|
||||
first: $first
|
||||
offset: $offset
|
||||
) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
@ -5,6 +5,7 @@ export const searchPosts = gql`
|
||||
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
|
||||
postCount
|
||||
posts {
|
||||
__typename
|
||||
id
|
||||
title
|
||||
content
|
||||
|
||||
25
backend/src/graphql/queries/unpinGroupPost.ts
Normal file
25
backend/src/graphql/queries/unpinGroupPost.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const unpinGroupPost = gql`
|
||||
mutation ($id: ID!) {
|
||||
unpinGroupPost(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinned
|
||||
pinnedAt
|
||||
groupPinned
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { GraphQLUpload } from 'graphql-upload'
|
||||
|
||||
export default {
|
||||
|
||||
@ -130,10 +130,13 @@ export const attachments = (config: S3Config) => {
|
||||
const { upload } = fileInput
|
||||
if (!upload) throw new UserInputError('Cannot find attachment for given resource')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const uploadFile = await upload
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
const { name: fileName, ext } = path.parse(uploadFile.filename)
|
||||
const uniqueFilename = `${uuid()}-${slug(fileName)}${ext}`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const url = await s3.uploadFile({
|
||||
...uploadFile,
|
||||
uniqueFilename,
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { revokeBadge } from '@graphql/queries/revokeBadge'
|
||||
import { rewardTrophyBadge } from '@graphql/queries/rewardTrophyBadge'
|
||||
import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected'
|
||||
import { setVerificationBadge } from '@graphql/queries/setVerificationBadge'
|
||||
import { User } from '@graphql/queries/User'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -800,24 +799,6 @@ describe('Badges', () => {
|
||||
describe('check test setup', () => {
|
||||
it('user has one badge and has it selected', async () => {
|
||||
authenticatedUser = await regularUser.toJson()
|
||||
const userQuery = gql`
|
||||
{
|
||||
User(id: "regular-user-id") {
|
||||
badgeTrophiesCount
|
||||
badgeTrophies {
|
||||
id
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
isDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = {
|
||||
data: {
|
||||
User: [
|
||||
@ -871,7 +852,9 @@ describe('Badges', () => {
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(query({ query: userQuery })).resolves.toMatchObject(expected)
|
||||
await expect(
|
||||
query({ query: User, variables: { id: 'regular-user-id' } }),
|
||||
).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
|
||||
@ -2,59 +2,39 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import { followUser } from '@graphql/queries/followUser'
|
||||
import { unfollowUser } from '@graphql/queries/unfollowUser'
|
||||
import createServer from '@src/server'
|
||||
import { User } from '@graphql/queries/User'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
let query
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let authenticatedUser: Context['user']
|
||||
const context = () => ({ authenticatedUser })
|
||||
let mutate: ApolloTestSetup['mutate']
|
||||
let query: ApolloTestSetup['query']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
let user1
|
||||
let user2
|
||||
let variables
|
||||
|
||||
const userQuery = gql`
|
||||
query ($id: ID) {
|
||||
User(id: $id) {
|
||||
followedBy {
|
||||
id
|
||||
}
|
||||
followedByCurrentUser
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => ({
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const testClient = createTestClient(server)
|
||||
query = testClient.query
|
||||
mutate = testClient.mutate
|
||||
const apolloSetup = createApolloTestSetup({ context })
|
||||
mutate = apolloSetup.mutate
|
||||
query = apolloSetup.query
|
||||
database = apolloSetup.database
|
||||
server = apolloSetup.server
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -129,7 +109,7 @@ describe('follow', () => {
|
||||
mutation: followUser,
|
||||
variables,
|
||||
})
|
||||
const relation = await neode.cypher(
|
||||
const relation = await database.neode.cypher(
|
||||
'MATCH (user:User {id: $id})-[relationship:FOLLOWS]->(followed:User) WHERE relationship.createdAt IS NOT NULL RETURN relationship',
|
||||
{ id: 'u1' },
|
||||
)
|
||||
@ -152,7 +132,7 @@ describe('follow', () => {
|
||||
}
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
query: User,
|
||||
variables: { id: user1.id },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
@ -891,8 +891,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
JoinGroup: {
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: {
|
||||
id: 'owner-of-closed-group',
|
||||
},
|
||||
membership: {
|
||||
role: 'usual',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -914,8 +918,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
JoinGroup: {
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'current-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -939,8 +947,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
JoinGroup: {
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: {
|
||||
id: 'current-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'pending',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -962,8 +974,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
JoinGroup: {
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'owner-of-closed-group',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1001,8 +1017,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
JoinGroup: {
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'owner-of-hidden-group',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1208,16 +1228,28 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1241,16 +1273,28 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1274,16 +1318,28 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1317,16 +1373,28 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'pending',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1350,16 +1418,28 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'pending',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1415,20 +1495,36 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'pending',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'admin',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'admin',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1452,20 +1548,36 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'pending',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'admin',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'admin',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1489,20 +1601,36 @@ describe('in mode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: expect.objectContaining({
|
||||
id: 'pending-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'pending',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'current-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: expect.objectContaining({
|
||||
id: 'current-user',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'usual',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
myRoleInGroup: 'admin',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-closed-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'admin',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
myRoleInGroup: 'owner',
|
||||
user: expect.objectContaining({
|
||||
id: 'owner-of-hidden-group',
|
||||
}),
|
||||
membership: expect.objectContaining({
|
||||
role: 'owner',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
@ -1600,8 +1728,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: {
|
||||
id: 'usual-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'usual',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1638,8 +1770,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'admin-member-user',
|
||||
myRoleInGroup: 'admin',
|
||||
user: {
|
||||
id: 'admin-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1673,8 +1809,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'second-owner-member-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'second-owner-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1759,8 +1899,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'owner-member-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'owner-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -1869,8 +2013,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'admin-member-user',
|
||||
myRoleInGroup: 'owner',
|
||||
user: {
|
||||
id: 'admin-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2047,8 +2195,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: 'admin',
|
||||
user: {
|
||||
id: 'usual-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2073,8 +2225,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: {
|
||||
id: 'usual-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'usual',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2234,8 +2390,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'pending-member-user',
|
||||
myRoleInGroup: 'usual',
|
||||
user: {
|
||||
id: 'pending-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'usual',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2260,8 +2420,12 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
ChangeGroupMemberRole: {
|
||||
id: 'pending-member-user',
|
||||
myRoleInGroup: 'pending',
|
||||
user: {
|
||||
id: 'pending-member-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'pending',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2413,7 +2577,7 @@ describe('in mode', () => {
|
||||
},
|
||||
})
|
||||
return result.data?.GroupMembers
|
||||
? !!result.data.GroupMembers.find((member) => member.id === userId)
|
||||
? !!result.data.GroupMembers.find((member) => member.user.id === userId)
|
||||
: null
|
||||
}
|
||||
|
||||
@ -2440,8 +2604,10 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'pending-member-user',
|
||||
myRoleInGroup: null,
|
||||
user: {
|
||||
id: 'pending-member-user',
|
||||
},
|
||||
membership: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2467,8 +2633,10 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: null,
|
||||
user: {
|
||||
id: 'usual-member-user',
|
||||
},
|
||||
membership: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -2494,8 +2662,10 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
LeaveGroup: {
|
||||
id: 'admin-member-user',
|
||||
myRoleInGroup: null,
|
||||
user: {
|
||||
id: 'admin-member-user',
|
||||
},
|
||||
membership: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -3021,8 +3191,10 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
RemoveUserFromGroup: expect.objectContaining({
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: null,
|
||||
user: expect.objectContaining({
|
||||
id: 'usual-member-user',
|
||||
}),
|
||||
membership: null,
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
@ -3093,8 +3265,10 @@ describe('in mode', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
RemoveUserFromGroup: expect.objectContaining({
|
||||
id: 'usual-member-user',
|
||||
myRoleInGroup: null,
|
||||
user: {
|
||||
id: 'usual-member-user',
|
||||
},
|
||||
membership: null,
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
|
||||
@ -24,9 +24,6 @@ export default {
|
||||
Query: {
|
||||
Group: async (_object, params, context: Context, _resolveInfo) => {
|
||||
const { isMember, id, slug, first, offset } = params
|
||||
let pagination = ''
|
||||
const orderBy = 'ORDER BY group.createdAt DESC'
|
||||
if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}`
|
||||
const matchParams = { id, slug }
|
||||
removeUndefinedNullValuesFromObject(matchParams)
|
||||
const session = context.driver.session()
|
||||
@ -34,43 +31,22 @@ export default {
|
||||
if (!context.user) {
|
||||
throw new Error('Missing authenticated user.')
|
||||
}
|
||||
const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true)
|
||||
let groupCypher
|
||||
if (isMember === true) {
|
||||
groupCypher = `
|
||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupMatchParamsCypher})
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
${orderBy}
|
||||
${pagination}
|
||||
const transactionResponse = await txc.run(
|
||||
`
|
||||
} else {
|
||||
if (isMember === false) {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
|
||||
WITH group
|
||||
WHERE group.groupType IN ['public', 'closed']
|
||||
RETURN group {.*, myRole: NULL}
|
||||
${orderBy}
|
||||
${pagination}
|
||||
`
|
||||
} else {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
${orderBy}
|
||||
${pagination}
|
||||
`
|
||||
}
|
||||
}
|
||||
const transactionResponse = await txc.run(groupCypher, {
|
||||
userId: context.user.id,
|
||||
})
|
||||
MATCH (group:Group${convertObjectToCypherMapLiteral(matchParams, true)})
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
WITH group, membership
|
||||
${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||
${(isMember === false && "WHERE membership IS NULL AND (group.groupType IN ['public', 'closed'])") || ''}
|
||||
${(isMember === undefined && "WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
ORDER BY group.createdAt DESC
|
||||
${first !== undefined && offset !== undefined ? `SKIP ${offset} LIMIT ${first}` : ''}
|
||||
`,
|
||||
{
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
return transactionResponse.records.map((record) => record.get('group'))
|
||||
})
|
||||
try {
|
||||
@ -87,7 +63,7 @@ export default {
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const groupMemberCypher = `
|
||||
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
|
||||
RETURN user {.*, myRoleInGroup: membership.role}
|
||||
RETURN user {.*}, membership {.*}
|
||||
SKIP toInteger($offset) LIMIT toInteger($first)
|
||||
`
|
||||
const transactionResponse = await txc.run(groupMemberCypher, {
|
||||
@ -95,7 +71,9 @@ export default {
|
||||
first,
|
||||
offset,
|
||||
})
|
||||
return transactionResponse.records.map((record) => record.get('user'))
|
||||
return transactionResponse.records.map((record) => {
|
||||
return { user: record.get('user'), membership: record.get('membership') }
|
||||
})
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
@ -297,8 +275,8 @@ export default {
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const joinGroupCypher = `
|
||||
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
|
||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
||||
MATCH (user:User {id: $userId}), (group:Group {id: $groupId})
|
||||
MERGE (user)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
@ -307,14 +285,15 @@ export default {
|
||||
THEN 'usual'
|
||||
ELSE 'pending'
|
||||
END
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
RETURN user {.*}, membership {.*}
|
||||
`
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
||||
const [member] = transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
return transactionResponse.records.map((record) => {
|
||||
return { user: record.get('user'), membership: record.get('membership') }
|
||||
})
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
return (await writeTxResultPromise)[0]
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
@ -361,7 +340,7 @@ export default {
|
||||
membership.updatedAt = toString(datetime()),
|
||||
membership.role = $roleInGroup
|
||||
${postRestrictionCypher}
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
RETURN member {.*} as user, membership {.*}
|
||||
`
|
||||
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, {
|
||||
@ -369,7 +348,9 @@ export default {
|
||||
userId,
|
||||
roleInGroup,
|
||||
})
|
||||
const [member] = transactionResponse.records.map((record) => record.get('member'))
|
||||
const [member] = transactionResponse.records.map((record) => {
|
||||
return { user: record.get('user'), membership: record.get('membership') }
|
||||
})
|
||||
return member
|
||||
})
|
||||
try {
|
||||
@ -460,6 +441,23 @@ export default {
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
myRole: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Group!')
|
||||
}
|
||||
return (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (:User {id: $user.id})-[membership:MEMBER_OF]->(group:Group {id: $parent.id})
|
||||
RETURN membership.role as role
|
||||
`,
|
||||
variables: {
|
||||
user: context.user,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
).records.map((r) => r.get('role'))[0]
|
||||
},
|
||||
inviteCodes: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Group!')
|
||||
@ -478,6 +476,18 @@ export default {
|
||||
})
|
||||
).records.map((r) => r.get('inviteCodes'))
|
||||
},
|
||||
currentlyPinnedPostsCount: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Group!')
|
||||
}
|
||||
const result = await context.database.query({
|
||||
query: `
|
||||
MATCH (:User)-[pinned:GROUP_PINNED]->(pinnedPosts:Post)-[:IN]->(:Group {id: $group.id})
|
||||
RETURN toString(count(pinnedPosts)) as count`,
|
||||
variables: { group: parent },
|
||||
})
|
||||
return result.records[0].get('count')
|
||||
},
|
||||
...Resolver('Group', {
|
||||
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
||||
hasMany: {
|
||||
@ -523,14 +533,16 @@ const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId)
|
||||
WITH user, collect(p) AS posts
|
||||
FOREACH (post IN posts |
|
||||
MERGE (user)-[:CANNOT_SEE]->(post))
|
||||
RETURN user {.*, myRoleInGroup: NULL}
|
||||
RETURN user {.*}, NULL as membership
|
||||
`
|
||||
|
||||
const transactionResponse = await transaction.run(removeUserFromGroupCypher, {
|
||||
groupId,
|
||||
userId,
|
||||
})
|
||||
const [user] = await transactionResponse.records.map((record) => record.get('user'))
|
||||
const [user] = await transactionResponse.records.map((record) => {
|
||||
return { user: record.get('user'), membership: record.get('membership') }
|
||||
})
|
||||
return user
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ import { getMutedUsers } from '@graphql/resolvers/users'
|
||||
|
||||
export const filterForMutedUsers = async (params, context) => {
|
||||
if (!context.user) return params
|
||||
// Skip mute filter for single post lookups (direct navigation by id or slug)
|
||||
if (params.id || params.slug) return params
|
||||
const [mutedUsers] = await Promise.all([getMutedUsers(context)])
|
||||
const mutedUsersIds = [...mutedUsers.map((user) => user.id)]
|
||||
if (!mutedUsersIds.length) return params
|
||||
|
||||
@ -84,9 +84,12 @@ export const images = (config: S3Config) => {
|
||||
|
||||
const uploadImageFile = async (uploadPromise: Promise<FileUpload> | undefined) => {
|
||||
if (!uploadPromise) return undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const upload = await uploadPromise
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
const { name, ext } = path.parse(upload.filename)
|
||||
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
return await s3.uploadFile({ ...upload, uniqueFilename })
|
||||
}
|
||||
|
||||
|
||||
@ -1089,16 +1089,24 @@ describe('redeemInviteCode', () => {
|
||||
data: {
|
||||
GroupMembers: expect.arrayContaining([
|
||||
{
|
||||
id: 'inviting-user',
|
||||
myRoleInGroup: 'owner',
|
||||
name: 'Inviting User',
|
||||
slug: 'inviting-user',
|
||||
user: {
|
||||
id: 'inviting-user',
|
||||
name: 'Inviting User',
|
||||
slug: 'inviting-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'other-user',
|
||||
myRoleInGroup: 'pending',
|
||||
name: 'Other User',
|
||||
slug: 'other-user',
|
||||
user: {
|
||||
id: 'other-user',
|
||||
name: 'Other User',
|
||||
slug: 'other-user',
|
||||
},
|
||||
membership: {
|
||||
role: 'pending',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
|
||||
@ -1,37 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import { UpdateUser } from '@graphql/queries/UpdateUser'
|
||||
import { User } from '@graphql/queries/User'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
let query, mutate, authenticatedUser
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
let authenticatedUser: Context['user']
|
||||
const context = () => ({ authenticatedUser })
|
||||
let mutate: ApolloTestSetup['mutate']
|
||||
let query: ApolloTestSetup['query']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
const apolloSetup = createApolloTestSetup({ context })
|
||||
mutate = apolloSetup.mutate
|
||||
query = apolloSetup.query
|
||||
database = apolloSetup.database
|
||||
server = apolloSetup.server
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
@ -43,17 +40,6 @@ describe('resolvers', () => {
|
||||
describe('Location', () => {
|
||||
describe('custom mutation, not handled by neo4j-graphql-js', () => {
|
||||
let variables
|
||||
const updateUserMutation = gql`
|
||||
mutation ($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
name
|
||||
location {
|
||||
name: nameRU
|
||||
nameEN
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
@ -78,12 +64,12 @@ describe('resolvers', () => {
|
||||
})
|
||||
|
||||
it('returns `null` if location translation is not available', async () => {
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
name: 'John Doughnut',
|
||||
location: {
|
||||
name: null,
|
||||
nameRU: null,
|
||||
nameEN: 'Paris',
|
||||
},
|
||||
},
|
||||
@ -95,15 +81,6 @@ describe('resolvers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const distanceToMeQuery = gql`
|
||||
query ($id: ID!) {
|
||||
User(id: $id) {
|
||||
location {
|
||||
distanceToMe
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
let user, myPlaceUser, otherPlaceUser, noCordsPlaceUser, noPlaceUser
|
||||
|
||||
describe('distanceToMe', () => {
|
||||
@ -191,21 +168,19 @@ describe('distanceToMe', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
const targetUser = await user.toJson()
|
||||
await expect(
|
||||
query({ query: distanceToMeQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
location: {
|
||||
distanceToMe: 0,
|
||||
},
|
||||
query({ query: User, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
expect.objectContaining({
|
||||
location: {
|
||||
distanceToMe: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -214,21 +189,19 @@ describe('distanceToMe', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
const targetUser = await myPlaceUser.toJson()
|
||||
await expect(
|
||||
query({ query: distanceToMeQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
location: {
|
||||
distanceToMe: 0,
|
||||
},
|
||||
query({ query: User, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
expect.objectContaining({
|
||||
location: {
|
||||
distanceToMe: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -237,21 +210,19 @@ describe('distanceToMe', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
const targetUser = await otherPlaceUser.toJson()
|
||||
await expect(
|
||||
query({ query: distanceToMeQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
location: {
|
||||
distanceToMe: 746,
|
||||
},
|
||||
query({ query: User, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
expect.objectContaining({
|
||||
location: {
|
||||
distanceToMe: 746,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -260,21 +231,19 @@ describe('distanceToMe', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
const targetUser = await noCordsPlaceUser.toJson()
|
||||
await expect(
|
||||
query({ query: distanceToMeQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
location: {
|
||||
distanceToMe: null,
|
||||
},
|
||||
query({ query: User, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
expect.objectContaining({
|
||||
location: {
|
||||
distanceToMe: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -283,19 +252,17 @@ describe('distanceToMe', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
const targetUser = await noPlaceUser.toJson()
|
||||
await expect(
|
||||
query({ query: distanceToMeQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
location: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
query({ query: User, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
expect.objectContaining({
|
||||
location: null,
|
||||
}),
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -165,44 +165,46 @@ describe('given some notifications', () => {
|
||||
|
||||
describe('no filters', () => {
|
||||
it('returns all notifications of current user', async () => {
|
||||
const expected = [
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have seen this comment mentioning already',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T15:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'Already seen post mention',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T17:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(query({ query: notifications, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
notifications: expect.arrayContaining(expected),
|
||||
notifications: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have seen this comment mentioning already',
|
||||
id: 'c1',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T15:33:48.651Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'Already seen post mention',
|
||||
id: 'p2',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T17:33:48.651Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
id: 'c2',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
id: 'p3',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
@ -211,33 +213,34 @@ describe('given some notifications', () => {
|
||||
|
||||
describe('filter for read: false', () => {
|
||||
it('returns only unread notifications of current user', async () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
notifications: expect.arrayContaining([
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
const response = await query({
|
||||
query: notifications,
|
||||
variables: { ...variables, read: false },
|
||||
})
|
||||
await expect(response).toMatchObject(expected)
|
||||
await expect(response).toMatchObject({
|
||||
data: {
|
||||
notifications: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
id: 'c2',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
id: 'p3',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
await expect(response.data?.notifications).toHaveLength(2) // double-check
|
||||
})
|
||||
|
||||
@ -394,11 +397,13 @@ describe('given some notifications', () => {
|
||||
{
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
from: { __typename: 'Comment', content: 'You have been mentioned in a comment' },
|
||||
id: 'mentioned_in_comment/c2/you',
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
from: { __typename: 'Post', content: 'You have been mentioned in a post' },
|
||||
id: 'mentioned_in_post/p3/you',
|
||||
read: true,
|
||||
},
|
||||
]),
|
||||
|
||||
368
backend/src/graphql/resolvers/posts.group.pin.spec.ts
Normal file
368
backend/src/graphql/resolvers/posts.group.pin.spec.ts
Normal file
@ -0,0 +1,368 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { ChangeGroupMemberRole } from '@graphql/queries/ChangeGroupMemberRole'
|
||||
import { CreateGroup } from '@graphql/queries/CreateGroup'
|
||||
import { CreatePost } from '@graphql/queries/CreatePost'
|
||||
import { pinGroupPost } from '@graphql/queries/pinGroupPost'
|
||||
import { profilePagePosts } from '@graphql/queries/profilePagePosts'
|
||||
import { unpinGroupPost } from '@graphql/queries/unpinGroupPost'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
const defaultConfig = {
|
||||
CATEGORIES_ACTIVE: false,
|
||||
}
|
||||
let config: Partial<Context['config']>
|
||||
|
||||
let anyUser
|
||||
let allGroupsUser
|
||||
let publicUser
|
||||
let publicAdminUser
|
||||
let authenticatedUser: Context['user']
|
||||
const context = () => ({ authenticatedUser, config })
|
||||
let mutate: ApolloTestSetup['mutate']
|
||||
let query: ApolloTestSetup['query']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const apolloSetup = createApolloTestSetup({ context })
|
||||
mutate = apolloSetup.mutate
|
||||
query = apolloSetup.query
|
||||
database = apolloSetup.database
|
||||
server = apolloSetup.server
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
config = { ...defaultConfig }
|
||||
authenticatedUser = null
|
||||
|
||||
anyUser = await Factory.build('user', {
|
||||
id: 'any-user',
|
||||
name: 'Any User',
|
||||
about: 'I am just an ordinary user and do not belong to any group.',
|
||||
})
|
||||
|
||||
allGroupsUser = await Factory.build('user', {
|
||||
id: 'all-groups-user',
|
||||
name: 'All Groups User',
|
||||
about: 'I am a member of all groups.',
|
||||
})
|
||||
publicUser = await Factory.build('user', {
|
||||
id: 'public-user',
|
||||
name: 'Public User',
|
||||
about: 'I am the owner of the public group.',
|
||||
})
|
||||
publicAdminUser = await Factory.build('user', {
|
||||
id: 'public-admin-user',
|
||||
name: 'Public Admin User',
|
||||
about: 'I am the admin of the public group.',
|
||||
})
|
||||
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
await mutate({
|
||||
mutation: CreateGroup,
|
||||
variables: {
|
||||
id: 'public-group',
|
||||
name: 'The Public Group',
|
||||
about: 'The public group!',
|
||||
description: 'Anyone can see the posts of this group.',
|
||||
groupType: 'public',
|
||||
actionRadius: 'regional',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: ChangeGroupMemberRole,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'all-groups-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: ChangeGroupMemberRole,
|
||||
variables: {
|
||||
groupId: 'public-group',
|
||||
userId: 'public-admin-user',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: ChangeGroupMemberRole,
|
||||
variables: {
|
||||
groupId: 'closed-group',
|
||||
userId: 'all-groups-user',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await anyUser.toJson()
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'post-without-group',
|
||||
title: 'A post without a group',
|
||||
content: 'I am a user who does not belong to a group yet.',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'post-1-to-public-group',
|
||||
title: 'Post 1 to a public group',
|
||||
content: 'I am posting into a public group as a member of the group',
|
||||
groupId: 'public-group',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'post-2-to-public-group',
|
||||
title: 'Post 1 to a public group',
|
||||
content: 'I am posting into a public group as a member of the group',
|
||||
groupId: 'public-group',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'post-3-to-public-group',
|
||||
title: 'Post 1 to a public group',
|
||||
content: 'I am posting into a public group as a member of the group',
|
||||
groupId: 'public-group',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('pin groupPosts', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinGroupPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ordinary users', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await anyUser.toJson()
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinGroupPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('group usual', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = await allGroupsUser.toJson()
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinGroupPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('group admin', () => {
|
||||
it('resolves without error', async () => {
|
||||
authenticatedUser = await publicAdminUser.toJson()
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: { pinGroupPost: { id: 'post-1-to-public-group', groupPinned: true } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('group owner', () => {
|
||||
it('resolves without error', async () => {
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: { pinGroupPost: { id: 'post-1-to-public-group', groupPinned: true } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('MAX_GROUP_PINNED_POSTS is 1', () => {
|
||||
beforeEach(async () => {
|
||||
config = { ...defaultConfig, MAX_GROUP_PINNED_POSTS: 1 }
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
})
|
||||
it('returns post-1-to-public-group as first, pinned post', async () => {
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await expect(
|
||||
query({
|
||||
query: profilePagePosts,
|
||||
variables: {
|
||||
filter: { group: { id: 'public-group' } },
|
||||
orderBy: ['groupPinned_asc', 'sortDate_desc'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
profilePagePosts: [
|
||||
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: null }),
|
||||
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: null }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('no error thrown when pinned post was pinned again', async () => {
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: { pinGroupPost: { id: 'post-1-to-public-group', groupPinned: true } },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns post-2-to-public-group as first, pinned post', async () => {
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||
await expect(
|
||||
query({
|
||||
query: profilePagePosts,
|
||||
variables: {
|
||||
filter: { group: { id: 'public-group' } },
|
||||
orderBy: ['groupPinned_asc', 'sortDate_desc'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
profilePagePosts: [
|
||||
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: null }),
|
||||
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: null }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns post-3-to-public-group as first, pinned post, when multiple are pinned', async () => {
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-3-to-public-group' } })
|
||||
await expect(
|
||||
query({
|
||||
query: profilePagePosts,
|
||||
variables: {
|
||||
filter: { group: { id: 'public-group' } },
|
||||
orderBy: ['groupPinned_asc', 'sortDate_desc'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
profilePagePosts: [
|
||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: null }),
|
||||
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: null }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('MAX_GROUP_PINNED_POSTS is 2', () => {
|
||||
beforeEach(async () => {
|
||||
config = { ...defaultConfig, MAX_GROUP_PINNED_POSTS: 2 }
|
||||
authenticatedUser = await publicUser.toJson()
|
||||
})
|
||||
it('returns post-1-to-public-group as first, post-2-to-public-group as second pinned post', async () => {
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||
await expect(
|
||||
query({
|
||||
query: profilePagePosts,
|
||||
variables: {
|
||||
filter: { group: { id: 'public-group' } },
|
||||
orderBy: ['groupPinned_asc', 'sortDate_desc'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
profilePagePosts: [
|
||||
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: null }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error when three posts are pinned', async () => {
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-3-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Reached maxed pinned posts already. Unpin a post first.' }],
|
||||
data: {
|
||||
pinGroupPost: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('throws no error when first unpinned before a third post is pinned', async () => {
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||
await mutate({ mutation: unpinGroupPost, variables: { id: 'post-1-to-public-group' } })
|
||||
await expect(
|
||||
mutate({ mutation: pinGroupPost, variables: { id: 'post-3-to-public-group' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
})
|
||||
await expect(
|
||||
query({
|
||||
query: profilePagePosts,
|
||||
variables: {
|
||||
filter: { group: { id: 'public-group' } },
|
||||
orderBy: ['groupPinned_asc', 'sortDate_desc'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
profilePagePosts: [
|
||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: true }),
|
||||
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: null }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { CreateComment } from '@graphql/queries/CreateComment'
|
||||
import { CreatePost } from '@graphql/queries/CreatePost'
|
||||
import { Post } from '@graphql/queries/Post'
|
||||
import { toggleObservePost } from '@graphql/queries/toggleObservePost'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
@ -20,25 +20,6 @@ let query: ApolloTestSetup['query']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
const createCommentMutation = gql`
|
||||
mutation ($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
isPostObservedByMe
|
||||
postObservingUsersCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const postQuery = gql`
|
||||
query Post($id: ID) {
|
||||
Post(id: $id) {
|
||||
isObservedByMe
|
||||
observingUsersCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const apolloSetup = createApolloTestSetup({ context })
|
||||
@ -101,7 +82,7 @@ describe('observing posts', () => {
|
||||
it('has another user NOT observing the post BEFORE commenting it', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: postQuery,
|
||||
query: Post,
|
||||
variables: { id: 'p2' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -120,7 +101,7 @@ describe('observing posts', () => {
|
||||
it('has another user observing the post AFTER commenting it', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createCommentMutation,
|
||||
mutation: CreateComment,
|
||||
variables: {
|
||||
postId: 'p2',
|
||||
content: 'After commenting the post, I should observe the post automatically',
|
||||
@ -137,7 +118,7 @@ describe('observing posts', () => {
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: postQuery,
|
||||
query: Post,
|
||||
variables: { id: 'p2' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -185,7 +166,7 @@ describe('observing posts', () => {
|
||||
it('does NOT alter the observation state', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createCommentMutation,
|
||||
mutation: CreateComment,
|
||||
variables: {
|
||||
postId: 'p2',
|
||||
content:
|
||||
@ -203,7 +184,7 @@ describe('observing posts', () => {
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: postQuery,
|
||||
query: Post,
|
||||
variables: { id: 'p2' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import Image from '@db/models/Image'
|
||||
import { AddPostEmotions } from '@graphql/queries/AddPostEmotions'
|
||||
@ -18,6 +16,7 @@ import { pushPost } from '@graphql/queries/pushPost'
|
||||
import { RemovePostEmotions } from '@graphql/queries/RemovePostEmotions'
|
||||
import { unpinPost } from '@graphql/queries/unpinPost'
|
||||
import { unpushPost } from '@graphql/queries/unpushPost'
|
||||
import { UpdatePost } from '@graphql/queries/UpdatePost'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -133,18 +132,14 @@ describe('Post', () => {
|
||||
|
||||
describe('no filter', () => {
|
||||
it('returns all posts', async () => {
|
||||
const postQueryNoFilters = gql`
|
||||
query Post($filter: _PostFilter) {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }]
|
||||
variables = { filter: {} }
|
||||
await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining(expected),
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'happy-post' }),
|
||||
expect.objectContaining({ id: 'cry-post' }),
|
||||
expect.objectContaining({ id: 'post-by-followed-user' }),
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -178,17 +173,6 @@ describe('Post', () => {
|
||||
}) */
|
||||
|
||||
describe('by emotions', () => {
|
||||
const postQueryFilteredByEmotions = gql`
|
||||
query Post($filter: _PostFilter) {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
emotions {
|
||||
emotion
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
it('filters by single emotion', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
@ -202,30 +186,25 @@ describe('Post', () => {
|
||||
}
|
||||
await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
|
||||
variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } }
|
||||
await expect(
|
||||
query({ query: postQueryFilteredByEmotions, variables }),
|
||||
).resolves.toMatchObject(expected)
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('filters by multiple emotions', async () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'happy-post',
|
||||
emotions: [{ emotion: 'happy' }],
|
||||
},
|
||||
{
|
||||
id: 'cry-post',
|
||||
emotions: [{ emotion: 'cry' }],
|
||||
},
|
||||
]
|
||||
await user.relateTo(happyPost, 'emoted', { emotion: 'happy' })
|
||||
await user.relateTo(cryPost, 'emoted', { emotion: 'cry' })
|
||||
variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } }
|
||||
await expect(
|
||||
query({ query: postQueryFilteredByEmotions, variables }),
|
||||
).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining(expected),
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'happy-post',
|
||||
emotions: [expect.objectContaining({ emotion: 'happy' })],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'cry-post',
|
||||
emotions: [expect.objectContaining({ emotion: 'cry' })],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
@ -233,22 +212,9 @@ describe('Post', () => {
|
||||
})
|
||||
|
||||
it('by followed-by', async () => {
|
||||
const postQueryFilteredByUsersFollowed = gql`
|
||||
query Post($filter: _PostFilter) {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
await user.relateTo(followedUser, 'following')
|
||||
variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } }
|
||||
await expect(
|
||||
query({ query: postQueryFilteredByUsersFollowed, variables }),
|
||||
).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
@ -655,48 +621,6 @@ describe('CreatePost', () => {
|
||||
|
||||
describe('UpdatePost', () => {
|
||||
let author, newlyCreatedPost
|
||||
const updatePostMutation = gql`
|
||||
mutation (
|
||||
$id: ID!
|
||||
$title: String!
|
||||
$content: String!
|
||||
$image: ImageInput
|
||||
$categoryIds: [ID]
|
||||
$postType: PostType
|
||||
$eventInput: _EventInput
|
||||
) {
|
||||
UpdatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
image: $image
|
||||
categoryIds: $categoryIds
|
||||
postType: $postType
|
||||
eventInput: $eventInput
|
||||
) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
categories {
|
||||
id
|
||||
}
|
||||
postType
|
||||
eventStart
|
||||
eventLocationName
|
||||
eventVenue
|
||||
eventLocation {
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
author = await Factory.build('user', { slug: 'the-author' })
|
||||
authenticatedUser = await author.toJson()
|
||||
@ -719,7 +643,7 @@ describe('UpdatePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: UpdatePost, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { UpdatePost: null },
|
||||
})
|
||||
@ -732,7 +656,7 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: updatePostMutation, variables })
|
||||
const { errors } = await mutate({ mutation: UpdatePost, variables })
|
||||
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -747,9 +671,7 @@ describe('UpdatePost', () => {
|
||||
data: { UpdatePost: { id: newlyCreatedPost.id, content: 'New content' } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdatePost, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('updates a post, but maintains non-updated attributes', async () => {
|
||||
@ -763,18 +685,16 @@ describe('UpdatePost', () => {
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdatePost, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('updates the updatedAt attribute', async () => {
|
||||
const {
|
||||
data: { UpdatePost },
|
||||
} = (await mutate({ mutation: updatePostMutation, variables })) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
expect(UpdatePost.updatedAt).toBeTruthy()
|
||||
expect(Date.parse(UpdatePost.updatedAt)).toEqual(expect.any(Number))
|
||||
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
|
||||
data: { UpdatePost: UpdatePostData },
|
||||
} = (await mutate({ mutation: UpdatePost, variables })) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
expect(UpdatePostData.updatedAt).toBeTruthy()
|
||||
expect(Date.parse(UpdatePostData.updatedAt)).toEqual(expect.any(Number))
|
||||
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePostData.updatedAt)
|
||||
})
|
||||
|
||||
describe('no new category ids provided for update', () => {
|
||||
@ -788,9 +708,7 @@ describe('UpdatePost', () => {
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdatePost, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -809,9 +727,7 @@ describe('UpdatePost', () => {
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdatePost, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -820,7 +736,7 @@ describe('UpdatePost', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: { ...variables, postType: 'Event' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -837,7 +753,7 @@ describe('UpdatePost', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: {
|
||||
...variables,
|
||||
postType: 'Event',
|
||||
@ -861,7 +777,7 @@ describe('UpdatePost', () => {
|
||||
const now = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: {
|
||||
...variables,
|
||||
postType: 'Event',
|
||||
@ -885,7 +801,7 @@ describe('UpdatePost', () => {
|
||||
const now = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: {
|
||||
...variables,
|
||||
postType: 'Event',
|
||||
@ -910,7 +826,7 @@ describe('UpdatePost', () => {
|
||||
const now = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: {
|
||||
...variables,
|
||||
postType: 'Event',
|
||||
@ -936,7 +852,7 @@ describe('UpdatePost', () => {
|
||||
const now = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updatePostMutation,
|
||||
mutation: UpdatePost,
|
||||
variables: {
|
||||
...variables,
|
||||
postType: 'Event',
|
||||
@ -976,7 +892,7 @@ describe('UpdatePost', () => {
|
||||
await expect(
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await mutate({ mutation: UpdatePost, variables })
|
||||
await expect(
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeTruthy()
|
||||
@ -989,7 +905,7 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
it('deletes the image', async () => {
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(6)
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await mutate({ mutation: UpdatePost, variables })
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(5)
|
||||
})
|
||||
})
|
||||
@ -1002,7 +918,7 @@ describe('UpdatePost', () => {
|
||||
await expect(
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await mutate({ mutation: UpdatePost, variables })
|
||||
await expect(
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
@ -2131,25 +2047,6 @@ describe('DeletePost', () => {
|
||||
|
||||
describe('emotions', () => {
|
||||
let author, postToEmote
|
||||
const PostsEmotionsCountQuery = gql`
|
||||
query ($id: ID!) {
|
||||
Post(id: $id) {
|
||||
emotionsCount
|
||||
}
|
||||
}
|
||||
`
|
||||
const PostsEmotionsQuery = gql`
|
||||
query ($id: ID!) {
|
||||
Post(id: $id) {
|
||||
emotions {
|
||||
emotion
|
||||
User {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
author = await database.neode.create('User', { id: 'u257' })
|
||||
@ -2226,8 +2123,8 @@ describe('emotions', () => {
|
||||
await mutate({ mutation: AddPostEmotions, variables })
|
||||
await mutate({ mutation: AddPostEmotions, variables })
|
||||
await expect(
|
||||
query({ query: PostsEmotionsCountQuery, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toEqual(expect.objectContaining(expected))
|
||||
query({ query: Post, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('allows a user to add more than one emotion', async () => {
|
||||
@ -2247,8 +2144,8 @@ describe('emotions', () => {
|
||||
variables = { ...variables, data: { emotion: 'surprised' } }
|
||||
await mutate({ mutation: AddPostEmotions, variables })
|
||||
await expect(
|
||||
query({ query: PostsEmotionsQuery, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toEqual(expect.objectContaining(expected))
|
||||
query({ query: Post, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2351,8 +2248,8 @@ describe('emotions', () => {
|
||||
variables: removePostEmotionsVariables,
|
||||
})
|
||||
await expect(
|
||||
query({ query: PostsEmotionsQuery, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toEqual(expect.objectContaining(expectedResponse))
|
||||
query({ query: Post, variables: postsEmotionsQueryVariables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,6 +29,20 @@ const maintainPinnedPosts = (params) => {
|
||||
return params
|
||||
}
|
||||
|
||||
const maintainGroupPinnedPosts = (params) => {
|
||||
// only show GroupPinnedPosts when Groups is selected
|
||||
if (!params.filter?.group) {
|
||||
return params
|
||||
}
|
||||
const pinnedPostFilter = { groupPinned: true, group: params.filter.group }
|
||||
if (isEmpty(params.filter)) {
|
||||
params.filter = { OR: [pinnedPostFilter, {}] }
|
||||
} else {
|
||||
params.filter = { OR: [pinnedPostFilter, { ...params.filter }] }
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const filterEventDates = (params) => {
|
||||
if (params.filter?.eventStart_gte) {
|
||||
const date = params.filter.eventStart_gte
|
||||
@ -52,6 +66,7 @@ export default {
|
||||
params = await filterPostsOfMyGroups(params, context)
|
||||
params = await filterInvisiblePosts(params, context)
|
||||
params = await filterForMutedUsers(params, context)
|
||||
params = await maintainGroupPinnedPosts(params)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => {
|
||||
@ -154,7 +169,7 @@ export default {
|
||||
)`
|
||||
}
|
||||
const categoriesCypher =
|
||||
config.CATEGORIES_ACTIVE && categoryIds
|
||||
config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > 0
|
||||
? `WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
@ -453,6 +468,68 @@ export default {
|
||||
}
|
||||
return unpinnedPost
|
||||
},
|
||||
pinGroupPost: async (_parent, params, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
throw new Error('Missing authenticated user.')
|
||||
}
|
||||
const { config } = context
|
||||
|
||||
if (config.MAX_GROUP_PINNED_POSTS === 0) {
|
||||
throw new Error('Pinned posts are not allowed!')
|
||||
}
|
||||
|
||||
// If MAX_GROUP_PINNED_POSTS === 1 -> Delete old pin
|
||||
if (config.MAX_GROUP_PINNED_POSTS === 1) {
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (post:Post {id: $params.id})-[:IN]->(group:Group)
|
||||
MATCH (:User)-[pinned:GROUP_PINNED]->(oldPinnedPost:Post)-[:IN]->(:Group {id: group.id})
|
||||
REMOVE oldPinnedPost.groupPinned
|
||||
DELETE pinned`,
|
||||
variables: { user: context.user, params },
|
||||
})
|
||||
// If MAX_GROUP_PINNED_POSTS !== 1 -> Check if max is reached
|
||||
} else {
|
||||
const result = await context.database.query({
|
||||
query: `
|
||||
MATCH (post:Post {id: $params.id})-[:IN]->(group:Group)
|
||||
MATCH (:User)-[pinned:GROUP_PINNED]->(pinnedPosts:Post)-[:IN]->(:Group {id: group.id})
|
||||
RETURN toString(count(pinnedPosts)) as count`,
|
||||
variables: { user: context.user, params },
|
||||
})
|
||||
if (result.records[0].get('count') >= config.MAX_GROUP_PINNED_POSTS) {
|
||||
throw new Error('Reached maxed pinned posts already. Unpin a post first.')
|
||||
}
|
||||
}
|
||||
|
||||
// Set new pin
|
||||
const result = await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})
|
||||
MATCH (post:Post {id: $params.id})-[:IN]->(group:Group)
|
||||
MERGE (user)-[pinned:GROUP_PINNED {createdAt: toString(datetime())}]->(post)
|
||||
SET post.groupPinned = true
|
||||
RETURN post {.*, pinnedAt: pinned.createdAt}`,
|
||||
variables: { user: context.user, params },
|
||||
})
|
||||
|
||||
// Return post
|
||||
return result.records[0].get('post')
|
||||
},
|
||||
unpinGroupPost: async (_parent, params, context, _resolveInfo) => {
|
||||
const result = await context.database.write({
|
||||
query: `
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (:User)-[pinned:GROUP_PINNED]->(post)
|
||||
DELETE pinned
|
||||
REMOVE post.groupPinned
|
||||
RETURN post {.*}`,
|
||||
variables: { postId: params.id },
|
||||
})
|
||||
|
||||
// Return post
|
||||
return result.records[0].get('post')
|
||||
},
|
||||
markTeaserAsViewed: async (_parent, params, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -550,6 +627,7 @@ export default {
|
||||
'language',
|
||||
'pinnedAt',
|
||||
'pinned',
|
||||
'groupPinned',
|
||||
'eventVenue',
|
||||
'eventLocation',
|
||||
'eventLocationName',
|
||||
@ -589,6 +667,21 @@ export default {
|
||||
'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1',
|
||||
},
|
||||
}),
|
||||
// As long as we rely on the filter capabilities of the neo4jgraphql library,
|
||||
// we cannot filter on a relation or their properties.
|
||||
// Hence we need to save the value to the group node in the database.
|
||||
/* groupPinned: async (parent, _params, context, _resolveInfo) => {
|
||||
return (
|
||||
(
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (:User)-[pinned:GROUP_PINNED]->(:Post {id: $parent.id})
|
||||
RETURN pinned`,
|
||||
variables: { parent },
|
||||
})
|
||||
).records.length === 1
|
||||
)
|
||||
}, */
|
||||
relatedContributions: async (parent, _params, context, _resolveInfo) => {
|
||||
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||
const { id } = parent
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import EmailAddress from '@db/models/EmailAddress'
|
||||
import User from '@db/models/User'
|
||||
import { Signup } from '@graphql/queries/Signup'
|
||||
import { SignupVerification } from '@graphql/queries/SignupVerification'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -158,31 +157,6 @@ describe('Signup', () => {
|
||||
})
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
const mutation = gql`
|
||||
mutation (
|
||||
$name: String!
|
||||
$password: String!
|
||||
$email: String!
|
||||
$nonce: String!
|
||||
$about: String
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
$locale: String
|
||||
) {
|
||||
SignupVerification(
|
||||
name: $name
|
||||
password: $password
|
||||
email: $email
|
||||
nonce: $nonce
|
||||
about: $about
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locale: $locale
|
||||
) {
|
||||
id
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('given valid password and email', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
@ -219,7 +193,9 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Invalid email or nonce' }],
|
||||
})
|
||||
})
|
||||
@ -237,7 +213,9 @@ describe('SignupVerification', () => {
|
||||
|
||||
describe('sending a valid nonce', () => {
|
||||
it('creates a user account', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
@ -247,7 +225,7 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
it('sets `verifiedAt` attribute of EmailAddress', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
await mutate({ mutation: SignupVerification, variables })
|
||||
const email = await database.neode.first(
|
||||
'EmailAddress',
|
||||
{ email: 'john@example.org' },
|
||||
@ -265,14 +243,14 @@ describe('SignupVerification', () => {
|
||||
MATCH(email:EmailAddress)-[:BELONGS_TO]->(u:User {name: $name})
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
await mutate({ mutation: SignupVerification, variables })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sets `about` attribute of User', async () => {
|
||||
variables = { ...variables, about: 'Find this description in the user profile' }
|
||||
await mutate({ mutation, variables })
|
||||
await mutate({ mutation: SignupVerification, variables })
|
||||
const user = await database.neode.first<typeof User>(
|
||||
'User',
|
||||
{ name: 'John Doe' },
|
||||
@ -285,7 +263,9 @@ describe('SignupVerification', () => {
|
||||
|
||||
it('allowing the about field to be an empty string', async () => {
|
||||
variables = { ...variables, about: '' }
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
@ -299,13 +279,15 @@ describe('SignupVerification', () => {
|
||||
MATCH(email:EmailAddress)<-[:PRIMARY_EMAIL]-(u:User {name: $name})
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
await mutate({ mutation: SignupVerification, variables })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('updates termsAndConditionsAgreedVersion', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: expect.objectContaining({
|
||||
termsAndConditionsAgreedVersion: '0.1.0',
|
||||
@ -315,7 +297,9 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
it('updates termsAndConditionsAgreedAt', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: expect.objectContaining({
|
||||
termsAndConditionsAgreedAt: expect.any(String),
|
||||
@ -326,7 +310,9 @@ describe('SignupVerification', () => {
|
||||
|
||||
it('rejects if version of terms and conditions is missing', async () => {
|
||||
variables = { ...variables, termsAndConditionsAgreedVersion: null }
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
@ -338,7 +324,9 @@ describe('SignupVerification', () => {
|
||||
|
||||
it('rejects if version of terms and conditions has wrong format', async () => {
|
||||
variables = { ...variables, termsAndConditionsAgreedVersion: 'invalid version format' }
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Invalid version format!' }],
|
||||
})
|
||||
})
|
||||
@ -350,7 +338,9 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({ mutation: SignupVerification, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Invalid email or nonce' }],
|
||||
})
|
||||
})
|
||||
|
||||
@ -14,7 +14,7 @@ import createServer from '@src/server'
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
describe('file a report on a resource', () => {
|
||||
describe('reports', () => {
|
||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||
const categoryIds = ['cat9']
|
||||
const variables = {
|
||||
@ -620,32 +620,31 @@ describe('file a report on a resource', () => {
|
||||
),
|
||||
])
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-post-1',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-comment-1',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-user-1',
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
},
|
||||
}),
|
||||
])
|
||||
// Sequential to ensure distinct createdAt values for orderBy tests
|
||||
await mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-post-1',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-comment-1',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: fileReport,
|
||||
variables: {
|
||||
resourceId: 'abusive-user-1',
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
},
|
||||
})
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
@ -660,82 +659,250 @@ describe('file a report on a resource', () => {
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
it('role "user" gets no reports', async () => {
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
await expect(query({ query: reports })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
})
|
||||
|
||||
it('returns no reports', async () => {
|
||||
await expect(query({ query: reports })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('role "moderator" gets reports', async () => {
|
||||
const expected = {
|
||||
reports: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'User',
|
||||
id: 'abusive-user-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('gets reports', async () => {
|
||||
const expected = {
|
||||
reports: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'User',
|
||||
id: 'abusive-user-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'doxing',
|
||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'Post',
|
||||
id: 'abusive-post-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'Post',
|
||||
id: 'abusive-post-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'Comment',
|
||||
id: 'abusive-comment-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
closed: false,
|
||||
resource: {
|
||||
__typename: 'Comment',
|
||||
id: 'abusive-comment-1',
|
||||
},
|
||||
filed: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
submitter: expect.objectContaining({
|
||||
id: 'current-user-id',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This comment is bigoted',
|
||||
}),
|
||||
createdAt: expect.any(String),
|
||||
reasonCategory: 'discrimination_etc',
|
||||
reasonDescription: 'This post is bigoted',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}
|
||||
authenticatedUser = await moderator.toJson()
|
||||
const { data } = await query({ query: reports })
|
||||
expect(data).toEqual(expected)
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}
|
||||
const { data } = await query({ query: reports })
|
||||
expect(data).toEqual(expected)
|
||||
})
|
||||
|
||||
describe('orderBy', () => {
|
||||
it('createdAt_asc returns reports in ascending order', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_asc' },
|
||||
})
|
||||
const sorted = [...data.reports].sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))
|
||||
expect(data.reports).toEqual(sorted)
|
||||
})
|
||||
|
||||
it('createdAt_desc returns reports in descending order', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_desc' },
|
||||
})
|
||||
const sorted = [...data.reports].sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
|
||||
expect(data.reports).toEqual(sorted)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reviewed filter', () => {
|
||||
it('reviewed: false returns only unreviewed reports', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { reviewed: false },
|
||||
})
|
||||
expect(data.reports).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('reviewed: true returns only reviewed reports', async () => {
|
||||
// review one report
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-post-1', disable: false, closed: false },
|
||||
})
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { reviewed: true },
|
||||
})
|
||||
expect(data.reports).toHaveLength(1)
|
||||
expect(data.reports[0].resource.id).toBe('abusive-post-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('closed filter', () => {
|
||||
it('closed: false returns only open reports', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { closed: false },
|
||||
})
|
||||
expect(data.reports).toHaveLength(3)
|
||||
data.reports.forEach((report) => {
|
||||
expect(report.closed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('closed: true returns only closed reports', async () => {
|
||||
// close one report via review
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
|
||||
})
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { closed: true },
|
||||
})
|
||||
expect(data.reports).toHaveLength(1)
|
||||
expect(data.reports[0].resource.id).toBe('abusive-post-1')
|
||||
expect(data.reports[0].closed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('combined reviewed and closed filter', () => {
|
||||
it('returns only reports matching both filters', async () => {
|
||||
// review and close one report
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
|
||||
})
|
||||
// review but keep open another report
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-user-1', disable: false, closed: false },
|
||||
})
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { reviewed: true, closed: true },
|
||||
})
|
||||
expect(data.reports).toHaveLength(1)
|
||||
expect(data.reports[0].resource.id).toBe('abusive-post-1')
|
||||
expect(data.reports[0].closed).toBe(true)
|
||||
})
|
||||
|
||||
it('reviewed: true, closed: false returns reviewed but open reports', async () => {
|
||||
// review and close one report
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
|
||||
})
|
||||
// review but keep open another report
|
||||
await mutate({
|
||||
mutation: review,
|
||||
variables: { resourceId: 'abusive-user-1', disable: false, closed: false },
|
||||
})
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { reviewed: true, closed: false },
|
||||
})
|
||||
expect(data.reports).toHaveLength(1)
|
||||
expect(data.reports[0].resource.id).toBe('abusive-user-1')
|
||||
expect(data.reports[0].closed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination', () => {
|
||||
it('first: 2 returns only 2 reports', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { first: 2 },
|
||||
})
|
||||
expect(data.reports).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('first: 1 returns only 1 report', async () => {
|
||||
const { data } = await query({
|
||||
query: reports,
|
||||
variables: { first: 1 },
|
||||
})
|
||||
expect(data.reports).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('offset: 1 skips the first report', async () => {
|
||||
const { data: allData } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_asc' },
|
||||
})
|
||||
const { data: offsetData } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_asc', offset: 1 },
|
||||
})
|
||||
expect(offsetData.reports).toHaveLength(allData.reports.length - 1)
|
||||
expect(offsetData.reports[0].id).toBe(allData.reports[1].id)
|
||||
})
|
||||
|
||||
it('first and offset combined for paging', async () => {
|
||||
const { data: allData } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_asc' },
|
||||
})
|
||||
const { data: pageData } = await query({
|
||||
query: reports,
|
||||
variables: { orderBy: 'createdAt_asc', first: 1, offset: 1 },
|
||||
})
|
||||
expect(pageData.reports).toHaveLength(1)
|
||||
expect(pageData.reports[0].id).toBe(allData.reports[1].id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -45,7 +45,8 @@ export default {
|
||||
reports: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let orderByClause, filterClause
|
||||
let orderByClause
|
||||
const filterClauses: string[] = []
|
||||
switch (params.orderBy) {
|
||||
case 'createdAt_asc':
|
||||
orderByClause = 'ORDER BY report.createdAt ASC'
|
||||
@ -59,26 +60,24 @@ export default {
|
||||
|
||||
switch (params.reviewed) {
|
||||
case true:
|
||||
filterClause = 'AND ((report)<-[:REVIEWED]-(:User))'
|
||||
filterClauses.push('AND ((report)<-[:REVIEWED]-(:User))')
|
||||
break
|
||||
case false:
|
||||
filterClause = 'AND NOT ((report)<-[:REVIEWED]-(:User))'
|
||||
filterClauses.push('AND NOT ((report)<-[:REVIEWED]-(:User))')
|
||||
break
|
||||
default:
|
||||
filterClause = ''
|
||||
}
|
||||
|
||||
switch (params.closed) {
|
||||
case true:
|
||||
filterClause = 'AND report.closed = true'
|
||||
filterClauses.push('AND report.closed = true')
|
||||
break
|
||||
case false:
|
||||
filterClause = 'AND report.closed = false'
|
||||
break
|
||||
default:
|
||||
filterClauses.push('AND report.closed = false')
|
||||
break
|
||||
}
|
||||
|
||||
const filterClause = filterClauses.join(' ')
|
||||
|
||||
const offset =
|
||||
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
||||
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
||||
@ -114,7 +113,8 @@ export default {
|
||||
},
|
||||
},
|
||||
Report: {
|
||||
filed: async (parent, _params, context, _resolveInfo) => {
|
||||
// This field is inline queried in the cypher statement above
|
||||
/* filed: async (parent, _params, context, _resolveInfo) => {
|
||||
if (typeof parent.filed !== 'undefined') return parent.filed
|
||||
const session = context.driver.session()
|
||||
const { id } = parent
|
||||
@ -146,9 +146,9 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
return filed
|
||||
},
|
||||
}, */
|
||||
reviewed: async (parent, _params, context, _resolveInfo) => {
|
||||
if (typeof parent.reviewed !== 'undefined') return parent.reviewed
|
||||
// if (typeof parent.reviewed !== 'undefined') return parent.reviewed
|
||||
const session = context.driver.session()
|
||||
const { id } = parent
|
||||
let reviewed
|
||||
|
||||
103
backend/src/graphql/resolvers/roles.spec.ts
Normal file
103
backend/src/graphql/resolvers/roles.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServerTestClient, createTestClient } from 'apollo-server-testing'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import { availableRoles } from '@graphql/queries/availableRoles'
|
||||
import createServer from '@src/server'
|
||||
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
describe('availableRoles', () => {
|
||||
let authenticatedUser
|
||||
let query: ApolloServerTestClient['query']
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
const { data, errors } = await query({ query: availableRoles })
|
||||
expect(data).toEqual(null)
|
||||
expect(errors).toEqual([expect.objectContaining({ message: 'Not Authorized!' })])
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('as user', () => {
|
||||
beforeEach(async () => {
|
||||
const user = await Factory.build(
|
||||
'user',
|
||||
{ id: 'user-id', role: 'user' },
|
||||
{ email: 'user@example.org', password: '1234' },
|
||||
)
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { data, errors } = await query({ query: availableRoles })
|
||||
expect(data).toEqual(null)
|
||||
expect(errors).toEqual([expect.objectContaining({ message: 'Not Authorized!' })])
|
||||
})
|
||||
})
|
||||
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const moderator = await Factory.build(
|
||||
'user',
|
||||
{ id: 'moderator-id', role: 'moderator' },
|
||||
{ email: 'moderator@example.org', password: '1234' },
|
||||
)
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { data, errors } = await query({ query: availableRoles })
|
||||
expect(data).toEqual(null)
|
||||
expect(errors).toEqual([expect.objectContaining({ message: 'Not Authorized!' })])
|
||||
})
|
||||
})
|
||||
|
||||
describe('as admin', () => {
|
||||
beforeEach(async () => {
|
||||
const admin = await Factory.build(
|
||||
'user',
|
||||
{ id: 'admin-id', role: 'admin' },
|
||||
{ email: 'admin@example.org', password: '1234' },
|
||||
)
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
it('returns available roles', async () => {
|
||||
const { data, errors } = await query({ query: availableRoles })
|
||||
expect(errors).toBeUndefined()
|
||||
expect(data?.availableRoles).toEqual(['admin', 'moderator', 'user'])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,10 +2,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { searchPosts } from '@graphql/queries/searchPosts'
|
||||
import { searchResults } from '@graphql/queries/searchResults'
|
||||
import createServer from '@src/server'
|
||||
|
||||
@ -34,19 +34,6 @@ afterAll(async () => {
|
||||
await driver.close()
|
||||
neode.close()
|
||||
})
|
||||
const searchPostQuery = gql`
|
||||
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
|
||||
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
|
||||
postCount
|
||||
posts {
|
||||
__typename
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('resolvers/searches', () => {
|
||||
let variables
|
||||
@ -605,7 +592,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
describe('query with limit 1', () => {
|
||||
it('has a count greater than 1', async () => {
|
||||
variables = { query: 'beitrag', firstPosts: 1, postsOffset: 0 }
|
||||
await expect(query({ query: searchPostQuery, variables })).resolves.toMatchObject({
|
||||
await expect(query({ query: searchPosts, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
searchPosts: {
|
||||
postCount: 2,
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { Post } from '@graphql/queries/Post'
|
||||
import { shout } from '@graphql/queries/shout'
|
||||
import { unshout } from '@graphql/queries/unshout'
|
||||
import createServer from '@src/server'
|
||||
@ -14,16 +14,6 @@ import createServer from '@src/server'
|
||||
let mutate, query, authenticatedUser, variables
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
const queryPost = gql`
|
||||
query ($id: ID!) {
|
||||
Post(id: $id) {
|
||||
id
|
||||
shoutedBy {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('shout and unshout posts', () => {
|
||||
let currentUser, postAuthor
|
||||
@ -38,6 +28,9 @@ describe('shout and unshout posts', () => {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -122,7 +115,7 @@ describe('shout and unshout posts', () => {
|
||||
await expect(mutate({ mutation: shout, variables })).resolves.toMatchObject({
|
||||
data: { shout: true },
|
||||
})
|
||||
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: { Post: [{ id: 'another-user-post-id', shoutedBy: [{ id: 'current-user-id' }] }] },
|
||||
errors: undefined,
|
||||
})
|
||||
@ -149,7 +142,7 @@ describe('shout and unshout posts', () => {
|
||||
await expect(mutate({ mutation: shout, variables })).resolves.toMatchObject({
|
||||
data: { shout: false },
|
||||
})
|
||||
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: { Post: [{ id: 'current-user-post-id', shoutedBy: [] }] },
|
||||
errors: undefined,
|
||||
})
|
||||
@ -191,7 +184,7 @@ describe('shout and unshout posts', () => {
|
||||
await expect(mutate({ mutation: unshout, variables })).resolves.toMatchObject({
|
||||
data: { unshout: true },
|
||||
})
|
||||
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
|
||||
await expect(query({ query: Post, variables })).resolves.toMatchObject({
|
||||
data: { Post: [{ id: 'posted-by-another-user', shoutedBy: [] }] },
|
||||
errors: undefined,
|
||||
})
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getDriver } from '@db/neo4j'
|
||||
import { CreateSocialMedia } from '@graphql/queries/CreateSocialMedia'
|
||||
import { DeleteSocialMedia } from '@graphql/queries/DeleteSocialMedia'
|
||||
import { UpdateSocialMedia } from '@graphql/queries/UpdateSocialMedia'
|
||||
import createServer from '@src/server'
|
||||
@ -84,24 +84,16 @@ describe('SocialMedia', () => {
|
||||
})
|
||||
|
||||
describe('create social media', () => {
|
||||
let mutation, variables
|
||||
let variables
|
||||
|
||||
beforeEach(() => {
|
||||
mutation = gql`
|
||||
mutation ($url: String!) {
|
||||
CreateSocialMedia(url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
variables = { url }
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const user = null
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
const result = await socialMediaAction(user, CreateSocialMedia, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
@ -115,21 +107,19 @@ describe('SocialMedia', () => {
|
||||
})
|
||||
|
||||
it('creates social media with the given url', async () => {
|
||||
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
CreateSocialMedia: {
|
||||
id: expect.any(String),
|
||||
url,
|
||||
},
|
||||
await expect(socialMediaAction(user, CreateSocialMedia, variables)).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateSocialMedia: {
|
||||
id: expect.any(String),
|
||||
url,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an empty string as url', async () => {
|
||||
variables = { url: '' }
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
const result = await socialMediaAction(user, CreateSocialMedia, variables)
|
||||
|
||||
expect(result.errors[0].message).toEqual(
|
||||
expect.stringContaining('"url" is not allowed to be empty'),
|
||||
@ -138,7 +128,7 @@ describe('SocialMedia', () => {
|
||||
|
||||
it('rejects invalid urls', async () => {
|
||||
variables = { url: 'not-a-url' }
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
const result = await socialMediaAction(user, CreateSocialMedia, variables)
|
||||
|
||||
expect(result.errors[0].message).toEqual(
|
||||
expect.stringContaining('"url" must be a valid uri'),
|
||||
@ -147,28 +137,13 @@ describe('SocialMedia', () => {
|
||||
})
|
||||
|
||||
describe('ownedBy', () => {
|
||||
beforeEach(() => {
|
||||
mutation = gql`
|
||||
mutation ($url: String!) {
|
||||
CreateSocialMedia(url: $url) {
|
||||
url
|
||||
ownedBy {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
it('resolves', async () => {
|
||||
const user = someUser
|
||||
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
CreateSocialMedia: { url, ownedBy: { name: 'Kalle Blomqvist' } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(socialMediaAction(user, CreateSocialMedia, variables)).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateSocialMedia: { url, ownedBy: { name: 'Kalle Blomqvist' } },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable jest/unbound-method */
|
||||
import gql from 'graphql-tag'
|
||||
import { verify } from 'jsonwebtoken'
|
||||
|
||||
import { categories } from '@constants/categories'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { changePassword } from '@graphql/queries/changePassword'
|
||||
import { currentUser } from '@graphql/queries/currentUser'
|
||||
import { login } from '@graphql/queries/login'
|
||||
import { saveCategorySettings } from '@graphql/queries/saveCategorySettings'
|
||||
import { decode } from '@jwt/decode'
|
||||
@ -86,24 +86,8 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('currentUser', () => {
|
||||
const currentUserQuery = gql`
|
||||
{
|
||||
currentUser {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
email
|
||||
role
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const respondsWith = async (expected) => {
|
||||
await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected)
|
||||
await expect(query({ query: currentUser, variables })).resolves.toMatchObject(expected)
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
@ -211,7 +195,7 @@ describe('currentUser', () => {
|
||||
})
|
||||
|
||||
it('returns only the saved active categories', async () => {
|
||||
const result = await query({ query: currentUserQuery, variables })
|
||||
const result = await query({ query: currentUser, variables })
|
||||
expect(result.data?.currentUser.activeCategories).toHaveLength(4)
|
||||
expect(result.data?.currentUser.activeCategories).toContain('cat1')
|
||||
expect(result.data?.currentUser.activeCategories).toContain('cat3')
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { AuthenticationError } from 'apollo-server'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { encode } from '@jwt/encode'
|
||||
@ -18,8 +15,21 @@ const neode = getNeode()
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
currentUser: async (object, params, context, resolveInfo) =>
|
||||
neo4jgraphql(object, { id: context.user.id }, context, resolveInfo),
|
||||
currentUser: async (_object, _params, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
throw new Error('You must be logged in')
|
||||
}
|
||||
const [user] = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:PRIMARY_EMAIL]->(e:EmailAddress)
|
||||
RETURN user {.*, email: e.email}
|
||||
`,
|
||||
variables: { user: context.user },
|
||||
})
|
||||
).records.map((record) => record.get('user'))
|
||||
return user
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
login: async (_, { email, password }, context: Context) => {
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { categories } from '@constants/categories'
|
||||
import pubsubContext from '@context/pubsub'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
@ -15,6 +13,8 @@ import { saveCategorySettings } from '@graphql/queries/saveCategorySettings'
|
||||
import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected'
|
||||
import { switchUserRole } from '@graphql/queries/switchUserRole'
|
||||
import { updateOnlineStatus } from '@graphql/queries/updateOnlineStatus'
|
||||
import { UpdateUser } from '@graphql/queries/UpdateUser'
|
||||
import { UserEmailNotificationSettings, User as userQuery } from '@graphql/queries/User'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -63,21 +63,12 @@ afterEach(async () => {
|
||||
|
||||
describe('User', () => {
|
||||
describe('query by email address', () => {
|
||||
let userQuery
|
||||
|
||||
beforeEach(async () => {
|
||||
const user = await Factory.build('user', {
|
||||
id: 'user',
|
||||
role: 'user',
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
userQuery = gql`
|
||||
query ($email: String) {
|
||||
User(email: $email) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
variables = {
|
||||
email: 'any-email-address@example.org',
|
||||
}
|
||||
@ -131,35 +122,7 @@ describe('User', () => {
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
let updateUserMutation
|
||||
|
||||
beforeEach(async () => {
|
||||
updateUserMutation = gql`
|
||||
mutation (
|
||||
$id: ID!
|
||||
$name: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$locationName: String # empty string '' sets it to null
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
name: $name
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
variables = {
|
||||
id: 'u47',
|
||||
name: 'John Doughnut',
|
||||
@ -196,8 +159,10 @@ describe('UpdateUser', () => {
|
||||
})
|
||||
|
||||
it('is not allowed to change other user accounts', async () => {
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -216,9 +181,7 @@ describe('UpdateUser', () => {
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
describe('given a new agreed version of terms and conditions', () => {
|
||||
@ -236,9 +199,7 @@ describe('UpdateUser', () => {
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -257,9 +218,7 @@ describe('UpdateUser', () => {
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@ -268,7 +227,7 @@ describe('UpdateUser', () => {
|
||||
...variables,
|
||||
termsAndConditionsAgreedVersion: 'invalid version format',
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
const { errors } = await mutate({ mutation: UpdateUser, variables })
|
||||
expect(errors?.[0]).toHaveProperty('message', 'Invalid version format!')
|
||||
})
|
||||
|
||||
@ -276,7 +235,7 @@ describe('UpdateUser', () => {
|
||||
describe('change location to "Hamburg, New Jersey, United States"', () => {
|
||||
it('has updated location to "Hamburg, New Jersey, United States"', async () => {
|
||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
locationName: 'Hamburg, New Jersey, United States',
|
||||
@ -295,7 +254,7 @@ describe('UpdateUser', () => {
|
||||
describe('change location to unset location', () => {
|
||||
it('has updated location to unset location', async () => {
|
||||
variables = { ...variables, locationName: '' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
locationName: null,
|
||||
@ -548,15 +507,10 @@ describe('switch user role', () => {
|
||||
id: 'user',
|
||||
role: 'admin',
|
||||
}
|
||||
await expect(mutate({ mutation: switchUserRole, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
await expect(mutate({ mutation: switchUserRole, variables })).resolves.toMatchObject({
|
||||
data: { switchUserRole: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -598,33 +552,6 @@ describe('switch user role', () => {
|
||||
})
|
||||
|
||||
let anotherUser
|
||||
const emailNotificationSettingsQuery = gql`
|
||||
query ($id: ID!) {
|
||||
User(id: $id) {
|
||||
emailNotificationSettings {
|
||||
type
|
||||
settings {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const emailNotificationSettingsMutation = gql`
|
||||
mutation ($id: ID!, $emailNotificationSettings: [EmailNotificationSettingsInput]!) {
|
||||
UpdateUser(id: $id, emailNotificationSettings: $emailNotificationSettings) {
|
||||
emailNotificationSettings {
|
||||
type
|
||||
settings {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('emailNotificationSettings', () => {
|
||||
beforeEach(async () => {
|
||||
@ -644,16 +571,11 @@ describe('emailNotificationSettings', () => {
|
||||
authenticatedUser = await anotherUser.toJson()
|
||||
const targetUser = await user.toJson()
|
||||
await expect(
|
||||
query({ query: emailNotificationSettingsQuery, variables: { id: targetUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
query({ query: UserEmailNotificationSettings, variables: { id: targetUser.id } }),
|
||||
).resolves.toMatchObject({
|
||||
data: { User: [null] },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -662,112 +584,13 @@ describe('emailNotificationSettings', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: emailNotificationSettingsQuery,
|
||||
query: UserEmailNotificationSettings,
|
||||
variables: { id: authenticatedUser?.id },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
emailNotificationSettings: [
|
||||
{
|
||||
type: 'post',
|
||||
settings: [
|
||||
{
|
||||
name: 'commentOnObservedPost',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'mention',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'followingUsers',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'postInGroup',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'chat',
|
||||
settings: [
|
||||
{
|
||||
name: 'chatMessage',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
settings: [
|
||||
{
|
||||
name: 'groupMemberJoined',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberLeft',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberRemoved',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberRoleChanged',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mutate the field', () => {
|
||||
const emailNotificationSettings = [{ name: 'mention', value: false }]
|
||||
|
||||
describe('as another user', () => {
|
||||
it('throws an error', async () => {
|
||||
authenticatedUser = await anotherUser.toJson()
|
||||
const targetUser = await user.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: emailNotificationSettingsMutation,
|
||||
variables: { id: targetUser.id, emailNotificationSettings },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('as self', () => {
|
||||
it('updates the emailNotificationSettings', async () => {
|
||||
authenticatedUser = (await user.toJson()) as DecodedUser
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: emailNotificationSettingsMutation,
|
||||
variables: { id: authenticatedUser.id, emailNotificationSettings },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
emailNotificationSettings: [
|
||||
{
|
||||
type: 'post',
|
||||
@ -778,7 +601,7 @@ describe('emailNotificationSettings', () => {
|
||||
},
|
||||
{
|
||||
name: 'mention',
|
||||
value: false,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'followingUsers',
|
||||
@ -822,9 +645,99 @@ describe('emailNotificationSettings', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mutate the field', () => {
|
||||
const emailNotificationSettings = [{ name: 'mention', value: false }]
|
||||
|
||||
describe('as another user', () => {
|
||||
it('throws an error', async () => {
|
||||
authenticatedUser = await anotherUser.toJson()
|
||||
const targetUser = await user.toJson()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: UpdateUser,
|
||||
variables: { id: targetUser.id, emailNotificationSettings },
|
||||
}),
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateUser: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('as self', () => {
|
||||
it('updates the emailNotificationSettings', async () => {
|
||||
authenticatedUser = (await user.toJson()) as DecodedUser
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: UpdateUser,
|
||||
variables: { id: authenticatedUser.id, emailNotificationSettings },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
emailNotificationSettings: [
|
||||
{
|
||||
type: 'post',
|
||||
settings: [
|
||||
{
|
||||
name: 'commentOnObservedPost',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'mention',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'followingUsers',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'postInGroup',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'chat',
|
||||
settings: [
|
||||
{
|
||||
name: 'chatMessage',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
settings: [
|
||||
{
|
||||
name: 'groupMemberJoined',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberLeft',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberRemoved',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'groupMemberRoleChanged',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -860,15 +773,10 @@ describe('save category settings', () => {
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toMatchObject({
|
||||
data: { saveCategorySettings: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -877,14 +785,6 @@ describe('save category settings', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
const userQuery = gql`
|
||||
query ($id: ID) {
|
||||
User(id: $id) {
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('no categories saved', () => {
|
||||
it('returns true for active categories mutation', async () => {
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||
@ -902,17 +802,15 @@ describe('save category settings', () => {
|
||||
it('returns the active categories when user is queried', async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { id: authenticatedUser?.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -944,23 +842,21 @@ describe('save category settings', () => {
|
||||
it('returns the new active categories when user is queried', async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { id: authenticatedUser?.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining([
|
||||
'cat10',
|
||||
'cat11',
|
||||
'cat12',
|
||||
'cat8',
|
||||
'cat9',
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining([
|
||||
'cat10',
|
||||
'cat11',
|
||||
'cat12',
|
||||
'cat8',
|
||||
'cat9',
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -984,15 +880,10 @@ describe('updateOnlineStatus', () => {
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toMatchObject({
|
||||
data: null,
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1122,15 +1013,10 @@ describe('setTrophyBadgeSelected', () => {
|
||||
mutation: setTrophyBadgeSelected,
|
||||
variables: { slot: 0, badgeId: 'trophy_bear' },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
).resolves.toMatchObject({
|
||||
data: { setTrophyBadgeSelected: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1500,15 +1386,10 @@ describe('resetTrophyBadgesSelected', () => {
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toMatchObject({
|
||||
data: { resetTrophyBadgesSelected: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
@ -353,14 +352,11 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
updateOnlineStatus: async (_object, args, context, _resolveInfo) => {
|
||||
updateOnlineStatus: async (_object, args, context: Context, _resolveInfo) => {
|
||||
const { status } = args
|
||||
const {
|
||||
user: { id },
|
||||
} = context
|
||||
|
||||
const CYPHER_AWAY = `
|
||||
MATCH (user:User {id: $id})
|
||||
MATCH (user:User {id: $user.id})
|
||||
WITH user,
|
||||
CASE user.lastOnlineStatus
|
||||
WHEN 'away' THEN user.awaySince
|
||||
@ -370,16 +366,14 @@ export default {
|
||||
SET user.lastOnlineStatus = $status
|
||||
`
|
||||
const CYPHER_ONLINE = `
|
||||
MATCH (user:User {id: $id})
|
||||
MATCH (user:User {id: $user.id})
|
||||
SET user.awaySince = null
|
||||
SET user.lastOnlineStatus = $status
|
||||
`
|
||||
|
||||
// Last Online Time is saved as `lastActiveAt`
|
||||
const session = context.driver.session()
|
||||
await session.writeTransaction((transaction) => {
|
||||
// return transaction.run(status === 'away' ? CYPHER_AWAY : CYPHER_ONLINE, { id, status })
|
||||
return transaction.run(status === 'away' ? CYPHER_AWAY : CYPHER_ONLINE, { id, status })
|
||||
await context.database.write({
|
||||
query: status === 'away' ? CYPHER_AWAY : CYPHER_ONLINE,
|
||||
variables: { user: context.user, status },
|
||||
})
|
||||
|
||||
return true
|
||||
@ -463,6 +457,18 @@ export default {
|
||||
},
|
||||
},
|
||||
User: {
|
||||
activeCategories: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
return (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (category:Category)
|
||||
WHERE NOT ((:User{id: $user.id})-[:NOT_INTERESTED_IN]->(category))
|
||||
RETURN collect(category.id) as categories
|
||||
`,
|
||||
variables: { user: parent },
|
||||
})
|
||||
).records.map((record) => record.get('categories'))[0]
|
||||
},
|
||||
inviteCodes: async (_parent, _args, context: Context, _resolveInfo) => {
|
||||
return (
|
||||
await context.database.query({
|
||||
@ -476,7 +482,7 @@ export default {
|
||||
})
|
||||
).records.map((record) => record.get('inviteCodes'))
|
||||
},
|
||||
emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => {
|
||||
emailNotificationSettings: (parent, _params, _context, _resolveInfo) => {
|
||||
return [
|
||||
{
|
||||
type: 'post',
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { cleanDatabase } from '@db/factories'
|
||||
import { blockedUsers } from '@graphql/queries/blockedUsers'
|
||||
import { blockUser } from '@graphql/queries/blockUser'
|
||||
import { Post } from '@graphql/queries/Post'
|
||||
import { unblockUser } from '@graphql/queries/unblockUser'
|
||||
import { User } from '@graphql/queries/User'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -155,56 +155,37 @@ describe('blockUser', () => {
|
||||
|
||||
it('unfollows the user when blocking', async () => {
|
||||
await currentUser.relateTo(blockedUser, 'following')
|
||||
const queryUser = gql`
|
||||
query {
|
||||
User(id: "u2") {
|
||||
id
|
||||
isBlocked
|
||||
followedByCurrentUser
|
||||
}
|
||||
}
|
||||
`
|
||||
await expect(query({ query: queryUser })).resolves.toMatchObject({
|
||||
await expect(query({ query: User, variables: { id: 'u2' } })).resolves.toMatchObject({
|
||||
data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] },
|
||||
})
|
||||
await mutate({ mutation: blockUser, variables: { id: 'u2' } })
|
||||
await expect(query({ query: queryUser })).resolves.toMatchObject({
|
||||
await expect(query({ query: User, variables: { id: 'u2' } })).resolves.toMatchObject({
|
||||
data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] },
|
||||
})
|
||||
})
|
||||
|
||||
describe('given both the current user and the to-be-blocked user write a post', () => {
|
||||
let postQuery
|
||||
|
||||
beforeEach(async () => {
|
||||
const post1 = await database.neode.create('Post', {
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
content: 'content',
|
||||
})
|
||||
const post2 = await database.neode.create('Post', {
|
||||
id: 'p23',
|
||||
title: 'A post written by the blocked user',
|
||||
content: 'content',
|
||||
})
|
||||
await Promise.all([
|
||||
post1.relateTo(currentUser, 'author'),
|
||||
post2.relateTo(blockedUser, 'author'),
|
||||
])
|
||||
postQuery = gql`
|
||||
query {
|
||||
Post(orderBy: createdAt_asc) {
|
||||
id
|
||||
title
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
const bothPostsAreInTheNewsfeed = async () => {
|
||||
await expect(query({ query: postQuery })).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
@ -238,7 +219,9 @@ describe('blockUser', () => {
|
||||
|
||||
// TODO: clarify proper behaviour
|
||||
it("the blocked user's post still shows up in the newsfeed of the current user", async () => {
|
||||
await expect(query({ query: postQuery })).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
@ -262,6 +245,81 @@ describe('blockUser', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('if the current user blocks and mutes the other user', () => {
|
||||
beforeEach(async () => {
|
||||
await currentUser.relateTo(blockedUser, 'blocked')
|
||||
await currentUser.relateTo(blockedUser, 'muted')
|
||||
})
|
||||
|
||||
it('the muted+blocked user post is still accessible by direct id lookup', async () => {
|
||||
await expect(query({ query: Post, variables: { id: 'p23' } })).resolves.toMatchObject(
|
||||
{
|
||||
data: {
|
||||
Post: [
|
||||
expect.objectContaining({
|
||||
id: 'p23',
|
||||
title: 'A post written by the blocked user',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('and the blocked+muted user has a pinned post', () => {
|
||||
beforeEach(async () => {
|
||||
const pinnedPost = await database.neode.create('Post', {
|
||||
id: 'p-pinned',
|
||||
title: 'A pinned post by the blocked user',
|
||||
content: 'pinned content',
|
||||
pinned: true,
|
||||
})
|
||||
await pinnedPost.relateTo(blockedUser, 'author')
|
||||
})
|
||||
|
||||
it('the pinned post is still accessible by id', async () => {
|
||||
await expect(
|
||||
query({ query: Post, variables: { id: 'p-pinned' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
expect.objectContaining({
|
||||
id: 'p-pinned',
|
||||
title: 'A pinned post by the blocked user',
|
||||
pinned: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('the pinned post shows up in the post list', async () => {
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'p-pinned',
|
||||
pinned: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('the non-pinned post from the muted+blocked user is still hidden in the feed', async () => {
|
||||
const result = await query({
|
||||
query: Post,
|
||||
variables: { orderBy: 'createdAt_asc' },
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
const postIds = result.data?.Post.map((p) => p.id)
|
||||
expect(postIds).not.toContain('p23')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('from the perspective of the blocked user', () => {
|
||||
@ -276,19 +334,21 @@ describe('blockUser', () => {
|
||||
})
|
||||
|
||||
it("the current user's post will show up in the newsfeed of the blocked user", async () => {
|
||||
await expect(query({ query: postQuery })).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: 'p23',
|
||||
title: 'A post written by the blocked user',
|
||||
author: { name: 'Blocked User', id: 'u2' },
|
||||
},
|
||||
{
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: { name: 'Current User', id: 'u1' },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { queryLocations } from '@graphql/queries/queryLocations'
|
||||
import { UpdateUser } from '@graphql/queries/UpdateUser'
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
@ -19,13 +18,6 @@ let query: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation ($id: ID!, $name: String!, $locationName: String) {
|
||||
UpdateUser(id: $id, name: $name, locationName: $locationName) {
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
const newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
@ -203,7 +195,7 @@ describe('userMiddleware', () => {
|
||||
name: 'Updating user',
|
||||
locationName: 'Welzheim, Baden-Württemberg, Germany',
|
||||
}
|
||||
await mutate({ mutation: updateUserMutation, variables })
|
||||
await mutate({ mutation: UpdateUser, variables })
|
||||
const locations = await database.neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(district:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city {.*}, state {.*}, country {.*}`,
|
||||
{},
|
||||
|
||||
@ -3,13 +3,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { mutedUsers } from '@graphql/queries/mutedUsers'
|
||||
import { muteUser } from '@graphql/queries/muteUser'
|
||||
import { Post } from '@graphql/queries/Post'
|
||||
import { unmuteUser } from '@graphql/queries/unmuteUser'
|
||||
import { User } from '@graphql/queries/User'
|
||||
import createServer from '@src/server'
|
||||
|
||||
const driver = getDriver()
|
||||
@ -152,85 +153,68 @@ describe('muteUser', () => {
|
||||
|
||||
it('unfollows the user', async () => {
|
||||
await currentUser.relateTo(mutedUser, 'following')
|
||||
const queryUser = gql`
|
||||
query {
|
||||
User(id: "u2") {
|
||||
id
|
||||
isMuted
|
||||
followedByCurrentUser
|
||||
}
|
||||
}
|
||||
`
|
||||
const { query } = createTestClient(server)
|
||||
await expect(query({ query: queryUser })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { User: [{ id: 'u2', isMuted: false, followedByCurrentUser: true }] },
|
||||
}),
|
||||
)
|
||||
await expect(query({ query: User, variables: { id: 'u2' } })).resolves.toMatchObject({
|
||||
data: {
|
||||
User: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'u2', isMuted: false, followedByCurrentUser: true }),
|
||||
]),
|
||||
},
|
||||
})
|
||||
await muteAction({ id: 'u2' })
|
||||
await expect(query({ query: queryUser })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { User: [{ id: 'u2', isMuted: true, followedByCurrentUser: false }] },
|
||||
}),
|
||||
)
|
||||
await expect(query({ query: User, variables: { id: 'u2' } })).resolves.toMatchObject({
|
||||
data: {
|
||||
User: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'u2', isMuted: true, followedByCurrentUser: false }),
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('given both the current user and the to-be-muted user write a post', () => {
|
||||
let postQuery
|
||||
|
||||
beforeEach(async () => {
|
||||
const post1 = await neode.create('Post', {
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
content: 'content',
|
||||
})
|
||||
const post2 = await neode.create('Post', {
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
content: 'content',
|
||||
})
|
||||
await Promise.all([
|
||||
post1.relateTo(currentUser, 'author'),
|
||||
post2.relateTo(mutedUser, 'author'),
|
||||
])
|
||||
postQuery = gql`
|
||||
query {
|
||||
Post(orderBy: createdAt_asc) {
|
||||
id
|
||||
title
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
const bothPostsAreInTheNewsfeed = async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: {
|
||||
name: 'Current User',
|
||||
id: 'u1',
|
||||
},
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: {
|
||||
name: 'Current User',
|
||||
id: 'u1',
|
||||
},
|
||||
{
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
author: {
|
||||
name: 'Muted User',
|
||||
id: 'u2',
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
author: {
|
||||
name: 'Muted User',
|
||||
id: 'u2',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('from the perspective of the current user', () => {
|
||||
@ -243,20 +227,93 @@ describe('muteUser', () => {
|
||||
|
||||
it("the muted user's post won't show up in the newsfeed of the current user", async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
expect.objectContaining({
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: { name: 'Current User', id: 'u1' },
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("the muted user's post is still accessible by direct id lookup", async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(query({ query: Post, variables: { id: 'p23' } })).resolves.toMatchObject(
|
||||
{
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: { name: 'Current User', id: 'u1' },
|
||||
},
|
||||
expect.objectContaining({
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('but the muted user has a pinned post', () => {
|
||||
beforeEach(async () => {
|
||||
const pinnedPost = await neode.create('Post', {
|
||||
id: 'p-pinned',
|
||||
title: 'A pinned post by the muted user',
|
||||
content: 'pinned content',
|
||||
pinned: true,
|
||||
})
|
||||
await pinnedPost.relateTo(mutedUser, 'author')
|
||||
})
|
||||
|
||||
it('the pinned post still shows up in the post list', async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'p-pinned',
|
||||
title: 'A pinned post by the muted user',
|
||||
pinned: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('the pinned post is accessible by id', async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({ query: Post, variables: { id: 'p-pinned' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
expect.objectContaining({
|
||||
id: 'p-pinned',
|
||||
title: 'A pinned post by the muted user',
|
||||
pinned: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('the non-pinned post from the muted user is still hidden in the feed', async () => {
|
||||
const { query } = createTestClient(server)
|
||||
const result = await query({
|
||||
query: Post,
|
||||
variables: { orderBy: 'createdAt_asc' },
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
const postIds = result.data?.Post.map((p) => p.id)
|
||||
expect(postIds).not.toContain('p23')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -273,24 +330,24 @@ describe('muteUser', () => {
|
||||
|
||||
it("the current user's post will show up in the newsfeed of the muted user", async () => {
|
||||
const { query } = createTestClient(server)
|
||||
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
{
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
author: { name: 'Muted User', id: 'u2' },
|
||||
},
|
||||
{
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: { name: 'Current User', id: 'u1' },
|
||||
},
|
||||
]),
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(
|
||||
query({ query: Post, variables: { orderBy: 'createdAt_asc' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'p23',
|
||||
title: 'A post written by the muted user',
|
||||
author: { name: 'Muted User', id: 'u2' },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'p12',
|
||||
title: 'A post written by the current user',
|
||||
author: { name: 'Current User', id: 'u1' },
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
||||
|
||||
import typeDefs from '@graphql/types/index'
|
||||
|
||||
14
backend/src/graphql/types/directive/neo4j-graphql-js.gql
Normal file
14
backend/src/graphql/types/directive/neo4j-graphql-js.gql
Normal file
@ -0,0 +1,14 @@
|
||||
# directive @MutationMeta on FIELD_DEFINITION
|
||||
# directive @isAuthenticated on FIELD_DEFINITION
|
||||
# directive @hasRole on FIELD_DEFINITION
|
||||
# directive @hasScope on FIELD_DEFINITION
|
||||
# directive @additionalLabels on FIELD_DEFINITION
|
||||
|
||||
directive @cypher(statement: String) on FIELD_DEFINITION
|
||||
directive @relation(
|
||||
name: String
|
||||
from: String
|
||||
to: String
|
||||
direction: String
|
||||
) on FIELD_DEFINITION | OBJECT
|
||||
directive @neo4j_ignore on FIELD_DEFINITION
|
||||
@ -50,12 +50,16 @@ type Comment {
|
||||
isPostObservedByMe: Boolean!
|
||||
@cypher(
|
||||
statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
||||
)
|
||||
)
|
||||
postObservingUsersCount: Int!
|
||||
@cypher(statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.disabled = true AND NOT u.deleted = true RETURN COUNT(DISTINCT u)")
|
||||
@cypher(
|
||||
statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.disabled = true AND NOT u.deleted = true RETURN COUNT(DISTINCT u)"
|
||||
)
|
||||
|
||||
shoutedByCurrentUser: Boolean!
|
||||
@cypher(statement: "MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))")
|
||||
@cypher(
|
||||
statement: "MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))"
|
||||
)
|
||||
|
||||
shoutedCount: Int!
|
||||
@cypher(
|
||||
@ -77,16 +81,7 @@ type Query {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreateComment(
|
||||
id: ID
|
||||
postId: ID!
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
): Comment
|
||||
UpdateComment(
|
||||
id: ID!
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
): Comment
|
||||
CreateComment(id: ID, postId: ID!, content: String!, contentExcerpt: String): Comment
|
||||
UpdateComment(id: ID!, content: String!, contentExcerpt: String): Comment
|
||||
DeleteComment(id: ID!): Comment
|
||||
}
|
||||
|
||||
@ -9,11 +9,7 @@ type Query {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
Signup(
|
||||
email: String!
|
||||
locale: String!
|
||||
inviteCode: String = null
|
||||
): EmailAddress
|
||||
Signup(email: String!, locale: String!, inviteCode: String = null): EmailAddress
|
||||
SignupVerification(
|
||||
nonce: String!
|
||||
email: String!
|
||||
@ -27,8 +23,5 @@ type Mutation {
|
||||
locationName: String = null
|
||||
): User
|
||||
AddEmailAddress(email: String!): EmailAddress
|
||||
VerifyEmailAddress(
|
||||
nonce: String!
|
||||
email: String!
|
||||
): EmailAddress
|
||||
VerifyEmailAddress(nonce: String!, email: String!): EmailAddress
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ type FILED {
|
||||
submitter: User
|
||||
}
|
||||
|
||||
# this list equals the strings of an array in file "webapp/constants/modals.js"
|
||||
"this list equals the strings of an array in file `webapp/constants/modals.js`"
|
||||
enum ReasonCategory {
|
||||
other
|
||||
discrimination_etc
|
||||
@ -26,5 +26,9 @@ type FiledReport {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): FiledReport
|
||||
fileReport(
|
||||
resourceId: ID!
|
||||
reasonCategory: ReasonCategory!
|
||||
reasonDescription: String!
|
||||
): FiledReport
|
||||
}
|
||||
@ -1,16 +1,16 @@
|
||||
type File {
|
||||
url: ID!,
|
||||
name: String,
|
||||
#size: Int,
|
||||
type: String,
|
||||
#audio: Boolean,
|
||||
#duration: Float,
|
||||
#preview: String,
|
||||
#progress: Int,
|
||||
url: ID!
|
||||
name: String
|
||||
type: String
|
||||
# size: Int
|
||||
# audio: Boolean
|
||||
# duration: Float
|
||||
# preview: String
|
||||
# progress: Int
|
||||
}
|
||||
|
||||
input FileInput {
|
||||
upload: Upload,
|
||||
name: String,
|
||||
type: String,
|
||||
upload: Upload
|
||||
name: String
|
||||
type: String
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user