mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-02-15 01:02:48 +00:00
Compare commits
136 Commits
b3.13.1-15
...
master
| 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 |
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"
|
day: "saturday"
|
||||||
timezone: "Europe/Berlin"
|
timezone: "Europe/Berlin"
|
||||||
time: "03:00"
|
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"
|
||||||
|
|||||||
5
.github/file-filters.yml
vendored
5
.github/file-filters.yml
vendored
@ -1,5 +1,9 @@
|
|||||||
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
|
# 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
|
backend: &backend
|
||||||
- '.github/workflows/test-backend.yml'
|
- '.github/workflows/test-backend.yml'
|
||||||
- 'backend/**/*'
|
- 'backend/**/*'
|
||||||
@ -14,6 +18,7 @@ webapp: &webapp
|
|||||||
- 'webapp/**/*'
|
- 'webapp/**/*'
|
||||||
- 'styleguide/**/*'
|
- 'styleguide/**/*'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
|
- *ui
|
||||||
|
|
||||||
docs-check: &docs-check
|
docs-check: &docs-check
|
||||||
- '.github/workflows/check-documentation.yml'
|
- '.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 }}
|
documentation: ${{ steps.changes.outputs.documentation }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Check for markdown file changes
|
- name: Check for markdown file changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
@ -28,13 +28,13 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.markdown == 'true'
|
if: needs.files-changed.outputs.markdown == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Remove uncheckable documentation files
|
- 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
|
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
|
- 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:
|
with:
|
||||||
use-quiet-mode: 'yes'
|
use-quiet-mode: 'yes'
|
||||||
use-verbose-mode: 'no'
|
use-verbose-mode: 'no'
|
||||||
@ -51,10 +51,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.documentation == 'true'
|
if: needs.files-changed.outputs.documentation == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Setup Node 20
|
- name: Setup Node 20
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4.0.3
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
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 }}
|
documentation: ${{ steps.changes.outputs.documentation }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
@ -27,10 +27,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Setup Node 20
|
- name: Setup Node 20
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4.0.3
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/docker-push.yml
vendored
6
.github/workflows/docker-push.yml
vendored
@ -59,16 +59,16 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
|||||||
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@ -14,9 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full History for changelog
|
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
|
- name: Setup env
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
@ -54,9 +58,13 @@ jobs:
|
|||||||
needs: [github_tag]
|
needs: [github_tag]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full History for changelog
|
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
|
- name: Setup env
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
@ -64,7 +72,7 @@ jobs:
|
|||||||
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
#- name: Repository Dispatch
|
#- name: Repository Dispatch
|
||||||
# uses: peter-evans/repository-dispatch@d2c43ab06ec1cddd2c2a0aae659681b8465ce87a # v3.0.0
|
# uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ github.token }}
|
# token: ${{ github.token }}
|
||||||
# event-type: trigger-ocelot-build-success
|
# 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}"}'
|
# 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
|
- name: Repository Dispatch stage.ocelot.social
|
||||||
uses: peter-evans/repository-dispatch@d2c43ab06ec1cddd2c2a0aae659681b8465ce87a # v3.0.0
|
uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
||||||
event-type: trigger-ocelot-build-success
|
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}"}'
|
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
|
- name: Repository Dispatch stage.yunite.me
|
||||||
uses: peter-evans/repository-dispatch@d2c43ab06ec1cddd2c2a0aae659681b8465ce87a # v3.0.0
|
uses: peter-evans/repository-dispatch@f49a8ac5751834a0666df77deb0289abbe2b3a78 # v3.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
|
||||||
event-type: trigger-ocelot-build-success
|
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 }}
|
backend: ${{ steps.changes.outputs.backend }}
|
||||||
docker: ${{ steps.changes.outputs.docker }}
|
docker: ${{ steps.changes.outputs.docker }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Check for backend file changes
|
- name: Check for backend file changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
@ -28,7 +28,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Neo4J | Build 'community' image
|
- name: Neo4J | Build 'community' image
|
||||||
run: |
|
run: |
|
||||||
@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
id: cache-neo4j
|
id: cache-neo4j
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/neo4j.tar
|
path: /tmp/neo4j.tar
|
||||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||||
@ -49,7 +49,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: backend | Build 'test' image
|
- name: backend | Build 'test' image
|
||||||
run: |
|
run: |
|
||||||
@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
id: cache-backend
|
id: cache-backend
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/backend.tar
|
path: /tmp/backend.tar
|
||||||
key: ${{ github.run_id }}-backend-cache
|
key: ${{ github.run_id }}-backend-cache
|
||||||
@ -70,7 +70,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- 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
|
- name: backend | Lint
|
||||||
run: cd backend && yarn && yarn run lint
|
run: cd backend && yarn && yarn run lint
|
||||||
@ -84,17 +89,17 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Restore Neo4J cache
|
- name: Restore Neo4J cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/neo4j.tar
|
path: /tmp/neo4j.tar
|
||||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
- name: Restore Backend cache
|
- name: Restore Backend cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/backend.tar
|
path: /tmp/backend.tar
|
||||||
key: ${{ github.run_id }}-backend-cache
|
key: ${{ github.run_id }}-backend-cache
|
||||||
@ -122,20 +127,3 @@ jobs:
|
|||||||
- name: backend | Unit test incl. coverage check
|
- name: backend | Unit test incl. coverage check
|
||||||
run: docker compose exec -T backend yarn test
|
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
|
|
||||||
|
|||||||
94
.github/workflows/test-e2e.yml
vendored
94
.github/workflows/test-e2e.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||||
|
|
||||||
- name: Copy backend env file
|
- name: Copy backend env file
|
||||||
run: |
|
run: |
|
||||||
@ -31,7 +31,7 @@ jobs:
|
|||||||
docker compose -f docker-compose.yml -f docker-compose.test.yml down
|
docker compose -f docker-compose.yml -f docker-compose.test.yml down
|
||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/tmp/backend.tar
|
/tmp/backend.tar
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||||
|
|
||||||
- name: Copy backend env file
|
- name: Copy backend env file
|
||||||
run: |
|
run: |
|
||||||
@ -59,7 +59,7 @@ jobs:
|
|||||||
docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
||||||
|
|
||||||
- name: Cache docker image
|
- name: Cache docker image
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/webapp.tar
|
path: /tmp/webapp.tar
|
||||||
key: ${{ github.run_id }}-e2e-webapp-cache
|
key: ${{ github.run_id }}-e2e-webapp-cache
|
||||||
@ -72,12 +72,12 @@ jobs:
|
|||||||
run: rm -rf /opt/hostedtoolcache
|
run: rm -rf /opt/hostedtoolcache
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4.4.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: 'backend/.tool-versions'
|
node-version-file: 'backend/.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Copy env files
|
- name: Copy env files
|
||||||
@ -87,7 +87,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cypress requirements
|
- name: Install cypress requirements
|
||||||
run: |
|
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
|
cd backend
|
||||||
yarn install
|
yarn install
|
||||||
yarn build
|
yarn build
|
||||||
@ -96,7 +97,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker image
|
- name: Cache docker image
|
||||||
|
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/opt/cucumber-json-formatter
|
/opt/cucumber-json-formatter
|
||||||
@ -104,32 +105,45 @@ jobs:
|
|||||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||||
key: ${{ github.run_id }}-e2e-cypress
|
key: ${{ github.run_id }}-e2e-cypress
|
||||||
|
|
||||||
fullstack_tests:
|
list_features:
|
||||||
name: Fullstack | tests
|
name: List Feature Files
|
||||||
if: success()
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare_backend_environment, prepare_webapp_image, prepare_cypress]
|
outputs:
|
||||||
|
features: ${{ steps.list.outputs.features }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
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
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
jobs: 8
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# run copies of the current job in parallel
|
feature: ${{ fromJson(needs.list_features.outputs.features) }}
|
||||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Delete huge unnecessary tools folder
|
- name: Delete huge unnecessary tools folder
|
||||||
run: rm -rf /opt/hostedtoolcache
|
run: rm -rf /opt/hostedtoolcache
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4.4.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: 'backend/.tool-versions'
|
node-version-file: 'backend/.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Restore cypress cache
|
- name: Restore cypress cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/opt/cucumber-json-formatter
|
/opt/cucumber-json-formatter
|
||||||
@ -139,7 +153,7 @@ jobs:
|
|||||||
restore-keys: ${{ github.run_id }}-e2e-cypress
|
restore-keys: ${{ github.run_id }}-e2e-cypress
|
||||||
|
|
||||||
- name: Restore backend environment cache
|
- name: Restore backend environment cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/tmp/backend.tar
|
/tmp/backend.tar
|
||||||
@ -150,7 +164,7 @@ jobs:
|
|||||||
key: ${{ github.run_id }}-e2e-backend-environment-cache
|
key: ${{ github.run_id }}-e2e-backend-environment-cache
|
||||||
|
|
||||||
- name: Restore webapp cache
|
- name: Restore webapp cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/webapp.tar
|
path: /tmp/webapp.tar
|
||||||
key: ${{ github.run_id }}-e2e-webapp-cache
|
key: ${{ github.run_id }}-e2e-webapp-cache
|
||||||
@ -170,7 +184,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Full stack tests | run tests
|
- name: Full stack tests | run tests
|
||||||
id: e2e-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
|
- name: Full stack tests | if tests failed, compile html report
|
||||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
@ -181,29 +195,21 @@ jobs:
|
|||||||
- name: Full stack tests | if tests failed, upload report
|
- name: Full stack tests | if tests failed, upload report
|
||||||
id: e2e-report
|
id: e2e-report
|
||||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
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
|
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
|
||||||
|
|
||||||
cleanup_cache:
|
e2e_status:
|
||||||
name: Cleanup Cache
|
name: E2E | Status
|
||||||
needs: fullstack_tests
|
if: always()
|
||||||
|
needs: [fullstack_tests]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Check E2E results
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
|
|
||||||
|
|
||||||
- name: Full stack tests | cleanup cache
|
|
||||||
run: |
|
run: |
|
||||||
cacheKeys=$(gh cache list --json key --jq '.[] | select(.key | startswith("${{ github.run_id }}-e2e-")) | .key')
|
if [ "${{ needs.fullstack_tests.result }}" != "success" ]; then
|
||||||
set +e
|
echo "E2E tests failed or were cancelled (result: ${{ needs.fullstack_tests.result }})"
|
||||||
echo "Deleting caches..."
|
exit 1
|
||||||
for cacheKey in $cacheKeys
|
fi
|
||||||
do
|
|
||||||
gh cache delete "$cacheKey"
|
|
||||||
done
|
|
||||||
echo "Done"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
50
.github/workflows/test-webapp.yml
vendored
50
.github/workflows/test-webapp.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
docker: ${{ steps.changes.outputs.docker }}
|
docker: ${{ steps.changes.outputs.docker }}
|
||||||
webapp: ${{ steps.changes.outputs.webapp }}
|
webapp: ${{ steps.changes.outputs.webapp }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Check for frontend file changes
|
- name: Check for frontend file changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
@ -28,7 +28,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- 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
|
- name: Check translation files
|
||||||
run: |
|
run: |
|
||||||
@ -42,15 +47,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Webapp | Build 'test' image
|
- name: Webapp | Build 'test' image
|
||||||
run: |
|
run: |
|
||||||
docker build --target test -f webapp/Dockerfile -t "ocelotsocialnetwork/webapp:test" .
|
docker build --target test -f webapp/Dockerfile -t "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" .
|
||||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
|
docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
||||||
|
|
||||||
- name: Cache docker image
|
- name: Cache docker image
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/webapp.tar
|
path: /tmp/webapp.tar
|
||||||
key: ${{ github.run_id }}-webapp-cache
|
key: ${{ github.run_id }}-webapp-cache
|
||||||
@ -62,7 +67,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- 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
|
- name: webapp | Lint
|
||||||
run: cd webapp && yarn && yarn run lint
|
run: cd webapp && yarn && yarn run lint
|
||||||
@ -76,10 +86,10 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.1.7
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7
|
||||||
|
|
||||||
- name: Restore webapp cache
|
- name: Restore webapp cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: /tmp/webapp.tar
|
path: /tmp/webapp.tar
|
||||||
key: ${{ github.run_id }}-webapp-cache
|
key: ${{ github.run_id }}-webapp-cache
|
||||||
@ -92,26 +102,8 @@ jobs:
|
|||||||
cp webapp/.env.template webapp/.env
|
cp webapp/.env.template webapp/.env
|
||||||
cp backend/.env.template backend/.env
|
cp backend/.env.template backend/.env
|
||||||
|
|
||||||
- name: backend | docker compose
|
- name: Start webapp container
|
||||||
# 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
|
||||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp --build
|
|
||||||
|
|
||||||
- name: webapp | Unit tests incl. coverage check
|
- name: webapp | Unit tests incl. coverage check
|
||||||
run: docker compose exec -T webapp yarn test
|
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
|
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/test.lint_pr.yml
vendored
1
.github/workflows/test.lint_pr.yml
vendored
@ -29,6 +29,7 @@ jobs:
|
|||||||
# Configure which scopes are allowed (newline delimited).
|
# Configure which scopes are allowed (newline delimited).
|
||||||
scopes: |
|
scopes: |
|
||||||
backend
|
backend
|
||||||
|
package/ui
|
||||||
webapp
|
webapp
|
||||||
maintenance
|
maintenance
|
||||||
database
|
database
|
||||||
|
|||||||
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
.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
|
|
||||||
160
CHANGELOG.md
160
CHANGELOG.md
@ -4,8 +4,168 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [3.14.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.14.0...3.14.1)
|
||||||
|
|
||||||
|
- refactor(package/ui): eslint config it4c update [`#9233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9233)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 from 3.985.0 to 3.990.0 in /backend [`#9224`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9224)
|
||||||
|
- build(deps-dev): bump eslint-plugin-vuejs-accessibility from 2.4.1 to 2.5.0 in /packages/ui [`#9226`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9226)
|
||||||
|
- build(deps-dev): bump eslint-plugin-storybook from 10.2.7 to 10.2.8 in /packages/ui [`#9228`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9228)
|
||||||
|
- build(deps-dev): bump glob from 13.0.1 to 13.0.3 in /packages/ui [`#9230`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9230)
|
||||||
|
- build(deps-dev): bump @types/node from 25.2.2 to 25.2.3 in /packages/ui [`#9232`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9232)
|
||||||
|
- fix(package/ui): os-button class to ensure branding compatibility [`#9211`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9211)
|
||||||
|
- refactor(webapp): vue3 migration - button - icon + circle + loading [`#9208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9208)
|
||||||
|
- fix(workflow): fix workflow not to double build the webapp image when running unit test [`#9210`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9210)
|
||||||
|
- build(deps-dev): bump the babel group with 3 updates [`#9109`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9109)
|
||||||
|
- build(deps): bump email-templates from 12.0.3 to 13.0.1 in /backend [`#9091`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9091)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jest from 29.12.2 to 29.13.0 in /backend [`#9186`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9186)
|
||||||
|
- fix(backend): fix categories filter [`#9209`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9209)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jsonc from 2.21.0 to 2.21.1 in /backend [`#9188`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9188)
|
||||||
|
- build(deps-dev): bump dotenv from 17.2.3 to 17.2.4 [`#9166`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9166)
|
||||||
|
- build(deps): bump actions/setup-node from 4 to 6 [`#9184`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9184)
|
||||||
|
- build(deps): bump actions/checkout from 4 to 6 [`#9185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9185)
|
||||||
|
- build(deps): bump actions/upload-artifact from 4 to 6 [`#9182`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9182)
|
||||||
|
- feat(webapp): correct version + commits [`#9203`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9203)
|
||||||
|
- fix(workflow): rename ui compatibility test [`#9207`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9207)
|
||||||
|
- refactor(workflow): cache packages [`#9206`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9206)
|
||||||
|
- refactor(workflow): all e2e are running in parallel [`#9205`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9205)
|
||||||
|
- fix(workflow): ensure ui workflows always run, but be skipped if not needed [`#9204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9204)
|
||||||
|
- build(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /backend [`#9187`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9187)
|
||||||
|
- fix(webapp): properly switch language on static pages [`#9202`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9202)
|
||||||
|
- fix(webapp): make static pages available when logged out [`#9201`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9201)
|
||||||
|
- refactor(webapp): vue3 migration - phase 3 - integration [`#9180`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9180)
|
||||||
|
- fix(backend): ensure a pinned post is accessible even tho the user was muted [`#9200`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9200)
|
||||||
|
- fix(backend): fix structure of unit test reports [`#9199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9199)
|
||||||
|
- refactor(package/ui): extract rules to eslint config it4c & update package [`#9198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9198)
|
||||||
|
- refactor(workflow): remove auto-approve workflow [`#9197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9197)
|
||||||
|
- refactor(workflow): add a new scope for PRs: package/ui [`#9196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9196)
|
||||||
|
- build(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /packages/ui [`#9193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9193)
|
||||||
|
- build(deps-dev): bump vue from 3.5.27 to 3.5.28 in /packages/ui in the vue group [`#9191`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9191)
|
||||||
|
- refactor(backend): reports query parameterization and resolver cleanup with test coverage [`#9156`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9156)
|
||||||
|
- fix(workflow): allow code rabbit to approve PRs [`#9195`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9195)
|
||||||
|
- feat(workflow): coderabbit [`#9194`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9194)
|
||||||
|
|
||||||
|
#### [3.14.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.13.1...3.14.0)
|
||||||
|
|
||||||
|
> 9 February 2026
|
||||||
|
|
||||||
|
- chore(release): v3.14.0 [`#9181`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9181)
|
||||||
|
- refactor(webapp): vue3 migration - phase 2 - setup [`#9161`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9161)
|
||||||
|
- build(deps): bump nginx from 1.29.4-alpine to 1.29.5-alpine in /webapp [`#9162`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9162)
|
||||||
|
- build(deps): bump node from 25.5.0-alpine to 25.6.0-alpine in /backend [`#9163`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9163)
|
||||||
|
- build(deps): bump node from 25.5.0-alpine to 25.6.0-alpine in /webapp [`#9164`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9164)
|
||||||
|
- build(deps-dev): bump cypress from 15.9.0 to 15.10.0 in the cypress group [`#9165`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9165)
|
||||||
|
- build(deps-dev): bump webpack from 5.104.1 to 5.105.0 [`#9167`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9167)
|
||||||
|
- build(deps): bump nodemailer from 7.0.13 to 8.0.0 in /backend [`#9168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9168)
|
||||||
|
- build(deps): bump minimatch from 10.1.1 to 10.1.2 in /backend [`#9169`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9169)
|
||||||
|
- build(deps-dev): bump @types/node from 25.1.0 to 25.2.1 in /backend [`#9171`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9171)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend [`#9173`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9173)
|
||||||
|
- refactor(workflow): do not clean cache after run [`#9155`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9155)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jest from 29.12.1 to 29.12.2 in /backend [`#9177`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9177)
|
||||||
|
- build(deps): bump peter-evans/repository-dispatch from cf70392543065ca62813db6712a06df1c4f4ae9f to f49a8ac5751834a0666df77deb0289abbe2b3a78 [`#9179`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9179)
|
||||||
|
- refactor(webapp): vue 2.7.16 [`#9160`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9160)
|
||||||
|
- refactor(backend): test roles [`#9157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9157)
|
||||||
|
- refactor(backend): middleware before/after [`#9128`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9128)
|
||||||
|
- build(deps): bump node from 25.4.0-alpine to 25.5.0-alpine in /webapp [`#9147`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9147)
|
||||||
|
- build(deps): bump node from 25.4.0-alpine to 25.5.0-alpine in /backend [`#9148`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9148)
|
||||||
|
- build(deps): bump actions/cache from 5.0.2 to 5.0.3 [`#9149`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9149)
|
||||||
|
- build(deps): bump docker/login-action from 3.6.0 to 3.7.0 [`#9150`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9150)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend [`#9151`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9151)
|
||||||
|
- build(deps-dev): bump @types/node from 25.0.10 to 25.1.0 in /backend [`#9152`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9152)
|
||||||
|
- refactor(backend): properly model group-membership [`#9124`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9124)
|
||||||
|
- fix(webapp): allow internal path for custom button [`#9129`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9129)
|
||||||
|
- feat(backend): db script disable notifications [`#9131`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9131)
|
||||||
|
- feat(backend): group pins [`#9034`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9034)
|
||||||
|
- refactor(backend): lint graphql [`#8473`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8473)
|
||||||
|
- fix(backend): fix bug in notifications settings for currentUser [`#9130`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9130)
|
||||||
|
- fix(backend): fix email url encoding [`#9127`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9127)
|
||||||
|
- refactor(other): consolidate Node.js versions and fix e2e workflow [`#9126`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9126)
|
||||||
|
- build(deps): bump cheerio from 1.1.2 to 1.2.0 in /backend [`#9141`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9141)
|
||||||
|
- build(deps-dev): bump @types/node from 25.0.9 to 25.0.10 in /backend [`#9142`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9142)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend [`#9144`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9144)
|
||||||
|
- build(deps): bump peter-evans/repository-dispatch from 09094272a794c6105029af051e3831908c649b6c to cf70392543065ca62813db6712a06df1c4f4ae9f [`#9145`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9145)
|
||||||
|
- build(deps): bump the metascraper group in /backend with 12 updates [`#9136`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9136)
|
||||||
|
- build(deps-dev): bump sass-embedded from 1.97.2 to 1.97.3 [`#9135`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9135)
|
||||||
|
- build(deps): bump preview-email from 3.1.0 to 3.1.1 in /backend [`#9138`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9138)
|
||||||
|
- fix(webapp): allow running frontend tests locally [`#9125`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9125)
|
||||||
|
- build(deps): bump node from 25.3.0-alpine to 25.4.0-alpine in /webapp [`#9133`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9133)
|
||||||
|
- build(deps-dev): bump @cucumber/cucumber from 12.5.0 to 12.6.0 in the cypress group [`#9134`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9134)
|
||||||
|
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /backend [`#9140`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9140)
|
||||||
|
- build(deps-dev): bump prettier from 3.8.0 to 3.8.1 in /webapp [`#9139`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9139)
|
||||||
|
- build(deps-dev): bump prettier from 3.8.0 to 3.8.1 in /backend [`#9143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9143)
|
||||||
|
- build(deps): bump actions/checkout from 6.0.1 to 6.0.2 [`#9146`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9146)
|
||||||
|
- build(deps): bump node from 25.3.0-alpine to 25.4.0-alpine in /backend [`#9132`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9132)
|
||||||
|
- feat(backend): admin creation command for production [`#9057`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9057)
|
||||||
|
- fix(backend): fix permissions for GroupInviteCodes [`#9121`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9121)
|
||||||
|
- fix(backend): fix group-myRole field query [`#9102`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9102)
|
||||||
|
- refactor(e2e): optimize step definitions loading with filepart pairing [`#9122`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9122)
|
||||||
|
- fix(webapp): fix cta-join-group, can crash when group is not defined [`#9103`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9103)
|
||||||
|
- fix(backend): fix active categories when inproperly configured [`#9123`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9123)
|
||||||
|
- fix(webapp): fix local webapp tests [`#9104`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9104)
|
||||||
|
- build(deps-dev): bump the cypress group across 1 directory with 3 updates [`#9058`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9058)
|
||||||
|
- build(deps): bump node from 25.2.1-alpine to 25.3.0-alpine in /webapp [`#9105`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9105)
|
||||||
|
- build(deps): bump actions/setup-node from 6.1.0 to 6.2.0 [`#9107`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9107)
|
||||||
|
- build(deps): bump node from 25.2.1-alpine to 25.3.0-alpine in /backend [`#9106`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9106)
|
||||||
|
- build(deps): bump actions/cache from 5.0.1 to 5.0.2 [`#9108`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9108)
|
||||||
|
- build(deps-dev): bump eslint-plugin-n from 17.23.1 to 17.23.2 in /backend [`#9110`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9110)
|
||||||
|
- build(deps-dev): bump @types/lodash from 4.17.21 to 4.17.23 in /backend [`#9111`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9111)
|
||||||
|
- build(deps): bump @aws-sdk/lib-storage from 3.958.0 to 3.967.0 in /backend [`#9113`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9113)
|
||||||
|
- build(deps-dev): bump eslint-plugin-prettier from 5.5.4 to 5.5.5 in /backend [`#9114`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9114)
|
||||||
|
- build(deps-dev): bump @eslint-community/eslint-plugin-eslint-comments from 4.5.0 to 4.6.0 in /backend [`#9120`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9120)
|
||||||
|
- build(deps-dev): bump prettier from 3.7.4 to 3.8.0 in /webapp [`#9117`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9117)
|
||||||
|
- build(deps): bump ioredis from 5.9.1 to 5.9.2 in /backend [`#9119`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9119)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 from 3.967.0 to 3.971.0 in /backend [`#9118`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9118)
|
||||||
|
- build(deps-dev): bump eslint-plugin-prettier from 5.5.4 to 5.5.5 in /webapp [`#9115`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9115)
|
||||||
|
- build(deps-dev): bump prettier from 3.7.4 to 3.8.0 in /backend [`#9116`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9116)
|
||||||
|
- build(deps-dev): bump @types/node from 25.0.7 to 25.0.9 in /backend [`#9112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9112)
|
||||||
|
- build(deps): bump the metascraper group in /backend with 12 updates [`#9063`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9063)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jest from 29.12.0 to 29.12.1 in /backend [`#9090`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9090)
|
||||||
|
- build(deps): bump ioredis from 5.8.2 to 5.9.1 in /backend [`#9095`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9095)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 from 3.958.0 to 3.966.0 in /backend [`#9100`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9100)
|
||||||
|
- build(deps): bump @aws-sdk/lib-storage from 3.933.0 to 3.958.0 in /backend [`#9093`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9093)
|
||||||
|
- build(deps-dev): bump @types/node from 25.0.3 to 25.0.5 in /backend [`#9096`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9096)
|
||||||
|
- build(deps): bump peter-evans/repository-dispatch from 46fabd2783425293d3f24bc1080da28d046e2dd3 to 09094272a794c6105029af051e3831908c649b6c [`#9089`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9089)
|
||||||
|
- build(deps): bump vue-advanced-chat from 2.0.11 to 2.1.2 in /webapp [`#9084`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9084)
|
||||||
|
- build(deps): bump nginx from 1.29.3-alpine to 1.29.4-alpine in /webapp [`#9070`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9070)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 from 3.933.0 to 3.958.0 in /backend [`#9086`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9086)
|
||||||
|
- build(deps-dev): bump @types/node from 24.10.1 to 25.0.3 in /backend [`#9078`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9078)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jest from 29.1.0 to 29.11.0 in /backend [`#9087`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9087)
|
||||||
|
- build(deps): bump express from 5.1.0 to 5.2.1 in /backend [`#9065`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9065)
|
||||||
|
- build(deps-dev): bump @types/lodash from 4.17.20 to 4.17.21 in /backend [`#9051`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9051)
|
||||||
|
- build(deps-dev): bump ts-jest from 29.4.5 to 29.4.6 in /backend [`#9069`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9069)
|
||||||
|
- build(deps): bump validator from 13.15.23 to 13.15.26 in /webapp [`#9083`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9083)
|
||||||
|
- build(deps): bump validator from 13.15.23 to 13.15.26 in /backend [`#9080`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9080)
|
||||||
|
- build(deps): bump nodemailer from 7.0.10 to 7.0.12 in /backend [`#9088`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9088)
|
||||||
|
- build(deps): bump actions/upload-artifact from 5.0.0 to 6.0.0 [`#9072`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9072)
|
||||||
|
- build(deps): bump peter-evans/repository-dispatch from d2c43ab06ec1cddd2c2a0aae659681b8465ce87a to 46fabd2783425293d3f24bc1080da28d046e2dd3 [`#9060`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9060)
|
||||||
|
- build(deps-dev): bump prettier from 3.6.2 to 3.7.4 in /webapp [`#9059`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9059)
|
||||||
|
- build(deps): bump docker/metadata-action from 5.9.0 to 5.10.0 [`#9049`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9049)
|
||||||
|
- build(deps): bump actions/setup-node from 6.0.0 to 6.1.0 [`#9061`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9061)
|
||||||
|
- build(deps): bump gaurav-nelson/github-action-markdown-link-check from 1.0.16 to 1.0.17 [`#8329`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8329)
|
||||||
|
- build(deps): bump actions/cache from 4.3.0 to 5.0.1 [`#9071`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9071)
|
||||||
|
- build(deps): bump actions/checkout from 5.0.0 to 6.0.1 [`#9062`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9062)
|
||||||
|
- build(deps-dev): bump prettier from 3.6.2 to 3.7.4 in /backend [`#9067`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9067)
|
||||||
|
- build(deps): bump mime-types from 3.0.1 to 3.0.2 in /backend [`#9044`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9044)
|
||||||
|
- build(deps): bump cross-env from 10.0.0 to 10.1.0 in /backend [`#8943`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8943)
|
||||||
|
- build(deps): bump validator from 13.15.20 to 13.15.23 in /webapp [`#9029`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9029)
|
||||||
|
- build(deps-dev): bump nodemon from 3.1.10 to 3.1.11 in /backend [`#9028`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9028)
|
||||||
|
- build(deps): bump node from 25.1.0-alpine to 25.2.0-alpine in /backend [`#9024`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9024)
|
||||||
|
- build(deps): bump node from 25.1.0-alpine to 25.2.0-alpine in /webapp [`#9023`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9023)
|
||||||
|
- build(deps): bump docker/metadata-action from 5.8.0 to 5.9.0 [`#9014`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9014)
|
||||||
|
- build(deps-dev): bump cypress from 15.5.0 to 15.6.0 in the cypress group [`#9016`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9016)
|
||||||
|
- build(deps): bump bcryptjs from 3.0.2 to 3.0.3 in /backend [`#9019`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9019)
|
||||||
|
- build(deps): bump @aws-sdk/lib-storage from 3.917.0 to 3.922.0 in /backend [`#9022`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9022)
|
||||||
|
- build(deps): bump peter-evans/repository-dispatch from 2c856c63feddee6147cab2f38801935b6a59a765 to d2c43ab06ec1cddd2c2a0aae659681b8465ce87a [`#9025`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9025)
|
||||||
|
- build(deps): bump amannn/action-semantic-pull-request from e49f57ce06c1747542fce2243c7a98682384bc0e to 069817c298f23fab00a8f29a2e556a5eac0f6390 [`#9026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9026)
|
||||||
|
- build(deps-dev): bump eslint-plugin-jest from 29.0.1 to 29.1.0 in /backend [`#9027`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9027)
|
||||||
|
- build(deps): bump @aws-sdk/client-s3 from 3.922.0 to 3.932.0 in /backend [`#9030`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9030)
|
||||||
|
- build(deps-dev): bump @types/node from 24.9.2 to 24.10.1 in /backend [`#9031`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9031)
|
||||||
|
- build(deps): bump validator from 13.15.20 to 13.15.23 in /backend [`#9033`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9033)
|
||||||
|
|
||||||
#### [3.13.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.13.0...3.13.1)
|
#### [3.13.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.13.0...3.13.1)
|
||||||
|
|
||||||
|
> 1 November 2025
|
||||||
|
|
||||||
|
- chore(release): v3.13.1 [`#9003`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9003)
|
||||||
- build(deps): bump nginx from 1.29.2-alpine to 1.29.3-alpine in /webapp [`#9005`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9005)
|
- build(deps): bump nginx from 1.29.2-alpine to 1.29.3-alpine in /webapp [`#9005`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9005)
|
||||||
- build(deps): bump minimatch from 10.0.3 to 10.1.1 in /backend [`#9009`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9009)
|
- build(deps): bump minimatch from 10.0.3 to 10.1.1 in /backend [`#9009`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9009)
|
||||||
- build(deps-dev): bump @types/node from 24.9.1 to 24.9.2 in /backend [`#9010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9010)
|
- build(deps-dev): bump @types/node from 24.9.1 to 24.9.2 in /backend [`#9010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9010)
|
||||||
|
|||||||
@ -48,3 +48,4 @@ IMAGOR_SECRET=mysecret
|
|||||||
|
|
||||||
CATEGORIES_ACTIVE=false
|
CATEGORIES_ACTIVE=false
|
||||||
MAX_PINNED_POSTS=1
|
MAX_PINNED_POSTS=1
|
||||||
|
MAX_GROUP_PINNED_POSTS=1
|
||||||
|
|||||||
@ -40,3 +40,4 @@ IMAGOR_SECRET=mysecret
|
|||||||
|
|
||||||
CATEGORIES_ACTIVE=false
|
CATEGORIES_ACTIVE=false
|
||||||
MAX_PINNED_POSTS=1
|
MAX_PINNED_POSTS=1
|
||||||
|
MAX_GROUP_PINNED_POSTS=1
|
||||||
|
|||||||
@ -14,7 +14,6 @@ module.exports = {
|
|||||||
'plugin:import/recommended',
|
'plugin:import/recommended',
|
||||||
'plugin:import/typescript',
|
'plugin:import/typescript',
|
||||||
'plugin:promise/recommended',
|
'plugin:promise/recommended',
|
||||||
'plugin:security/recommended-legacy',
|
|
||||||
'plugin:@eslint-community/eslint-comments/recommended',
|
'plugin:@eslint-community/eslint-comments/recommended',
|
||||||
'prettier',
|
'prettier',
|
||||||
],
|
],
|
||||||
@ -175,6 +174,10 @@ module.exports = {
|
|||||||
'@eslint-community/eslint-comments/require-description': 'off',
|
'@eslint-community/eslint-comments/require-description': 'off',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.js', '*.cjs', '*.ts', '*.tsx'],
|
||||||
|
extends: ['plugin:security/recommended-legacy'],
|
||||||
|
},
|
||||||
// only for ts files
|
// only for ts files
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.ts', '*.tsx'],
|
||||||
@ -228,5 +231,33 @@ module.exports = {
|
|||||||
files: ['*.json', '*.json5', '*.jsonc'],
|
files: ['*.json', '*.json5', '*.jsonc'],
|
||||||
parser: 'jsonc-eslint-parser',
|
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:25.2.1-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.name="ocelot.social:backend"
|
||||||
LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
|
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"
|
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/middlewares/ src/middleware/branding/
|
||||||
ONBUILD COPY ./branding/data/ src/db/data
|
ONBUILD COPY ./branding/data/ src/db/data
|
||||||
ONBUILD COPY ./branding/public/ public/
|
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 yarn run build
|
||||||
ONBUILD RUN mkdir /build
|
ONBUILD RUN mkdir /build
|
||||||
ONBUILD RUN cp -r ./build /build
|
ONBUILD RUN cp -r ./build /build
|
||||||
ONBUILD RUN cp -r ./public /build
|
ONBUILD RUN cp -r ./public /build
|
||||||
ONBUILD RUN cp -r ./package.json yarn.lock /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
|
FROM build AS test
|
||||||
# required for the migrations
|
# 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
|
## Installation without Docker
|
||||||
|
|
||||||
For the local installation you need a recent version of
|
For the local installation you need a recent version of
|
||||||
[Node](https://nodejs.org/en/) (>= `v16.19.0`). We are using
|
[Node](https://nodejs.org/en/). We are using
|
||||||
`v24.2.0` and therefore we recommend to use the same version
|
`v25.3.0` and therefore we recommend to use the same version. You can use the
|
||||||
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
|
|
||||||
some known problems with more recent node versions). You can use the
|
|
||||||
[node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
[node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||||
between different local Node versions:
|
between different local Node versions:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# install Node
|
# install Node using '.nvmrc' file
|
||||||
$ cd backend
|
$ cd backend
|
||||||
$ nvm install v24.2.0
|
$ nvm install
|
||||||
$ nvm use v24.2.0
|
$ nvm use
|
||||||
```
|
```
|
||||||
|
|
||||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social-backend",
|
"name": "ocelot-social-backend",
|
||||||
"version": "3.13.1",
|
"version": "3.14.1",
|
||||||
"description": "GraphQL Backend for ocelot.social",
|
"description": "GraphQL Backend for ocelot.social",
|
||||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
@ -12,7 +12,7 @@
|
|||||||
"build": "tsc && tsc-alias && ./scripts/build.copy.files.sh",
|
"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": "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",
|
"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",
|
"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": "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",
|
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
|
||||||
@ -23,24 +23,27 @@
|
|||||||
"db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts",
|
"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": "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: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: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: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": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.933.0",
|
"@aws-sdk/client-s3": "^3.990.0",
|
||||||
"@aws-sdk/lib-storage": "^3.933.0",
|
"@aws-sdk/lib-storage": "^3.985.0",
|
||||||
"@sentry/node": "^5.30.0",
|
"@sentry/node": "^5.30.0",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"apollo-server": "~2.14.2",
|
"apollo-server": "~2.14.2",
|
||||||
"apollo-server-express": "^2.14.2",
|
"apollo-server-express": "^2.14.2",
|
||||||
"bcryptjs": "~3.0.3",
|
"bcryptjs": "~3.0.3",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"cheerio": "~1.1.2",
|
"cheerio": "~1.2.0",
|
||||||
"cross-env": "~10.1.0",
|
"cross-env": "~10.1.0",
|
||||||
"dotenv": "~17.0.1",
|
"dotenv": "~17.0.1",
|
||||||
"email-templates": "^12.0.3",
|
"email-templates": "^13.0.1",
|
||||||
"express": "^5.1.0",
|
"express": "^4.22.1",
|
||||||
"graphql": "^14.6.0",
|
"graphql": "^14.6.0",
|
||||||
"graphql-middleware": "~6.1.35",
|
"graphql-middleware": "~6.1.35",
|
||||||
"graphql-middleware-sentry": "^3.2.1",
|
"graphql-middleware-sentry": "^3.2.1",
|
||||||
@ -50,55 +53,56 @@
|
|||||||
"graphql-tag": "~2.10.3",
|
"graphql-tag": "~2.10.3",
|
||||||
"graphql-upload": "^13.0.0",
|
"graphql-upload": "^13.0.0",
|
||||||
"helmet": "~8.1.0",
|
"helmet": "~8.1.0",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.9.2",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"languagedetect": "^2.0.0",
|
"languagedetect": "^2.0.0",
|
||||||
"linkify-html": "^4.3.2",
|
"linkify-html": "^4.3.2",
|
||||||
"linkifyjs": "^4.3.2",
|
"linkifyjs": "^4.3.2",
|
||||||
"lodash": "~4.17.21",
|
"lodash": "~4.17.23",
|
||||||
"merge-graphql-schemas": "^1.7.8",
|
"merge-graphql-schemas": "^1.7.8",
|
||||||
"metascraper": "^5.49.5",
|
"metascraper": "^5.49.19",
|
||||||
"metascraper-author": "^5.49.5",
|
"metascraper-author": "^5.49.19",
|
||||||
"metascraper-date": "^5.49.5",
|
"metascraper-date": "^5.49.19",
|
||||||
"metascraper-description": "^5.49.5",
|
"metascraper-description": "^5.49.19",
|
||||||
"metascraper-image": "^5.49.5",
|
"metascraper-image": "^5.49.19",
|
||||||
"metascraper-lang": "^5.49.5",
|
"metascraper-lang": "^5.49.19",
|
||||||
"metascraper-lang-detector": "^4.10.2",
|
"metascraper-lang-detector": "^4.10.2",
|
||||||
"metascraper-logo": "^5.49.5",
|
"metascraper-logo": "^5.49.19",
|
||||||
"metascraper-publisher": "^5.49.5",
|
"metascraper-publisher": "^5.49.19",
|
||||||
"metascraper-soundcloud": "^5.34.4",
|
"metascraper-soundcloud": "^5.34.4",
|
||||||
"metascraper-title": "^5.49.5",
|
"metascraper-title": "^5.49.19",
|
||||||
"metascraper-url": "^5.49.5",
|
"metascraper-url": "^5.49.19",
|
||||||
"metascraper-video": "^5.49.5",
|
"metascraper-video": "^5.49.19",
|
||||||
"metascraper-youtube": "^5.49.5",
|
"metascraper-youtube": "^5.49.20",
|
||||||
"migrate": "^2.1.0",
|
"migrate": "^2.1.0",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"minimatch": "^10.1.1",
|
"minimatch": "^10.1.2",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"neo4j-driver": "^4.4.11",
|
"neo4j-driver": "^4.4.11",
|
||||||
"neo4j-graphql-js": "^2.11.5",
|
"neo4j-graphql-js": "2.11.5",
|
||||||
"neode": "^0.4.9",
|
"neode": "^0.4.9",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^8.0.1",
|
||||||
"nodemailer-html-to-text": "^3.2.0",
|
"nodemailer-html-to-text": "^3.2.0",
|
||||||
"preview-email": "^3.1.0",
|
"preview-email": "^3.1.1",
|
||||||
"pug": "^3.0.3",
|
"pug": "^3.0.3",
|
||||||
"sanitize-html": "~2.17.0",
|
"sanitize-html": "~2.17.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"trunc-html": "~1.1.2",
|
"trunc-html": "~1.1.2",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"uuid": "~9.0.1",
|
"uuid": "~9.0.1",
|
||||||
"validator": "^13.15.23",
|
"validator": "^13.15.26",
|
||||||
"xregexp": "^5.1.2"
|
"xregexp": "^5.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@faker-js/faker": "9.9.0",
|
||||||
|
"@graphql-eslint/eslint-plugin": "^3.20.1",
|
||||||
"@types/email-templates": "^10.0.4",
|
"@types/email-templates": "^10.0.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/jsonwebtoken": "~8.5.1",
|
"@types/jsonwebtoken": "~8.5.1",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.23",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^25.2.3",
|
||||||
"@types/request": "^2.48.13",
|
"@types/request": "^2.48.13",
|
||||||
"@types/slug": "^5.0.9",
|
"@types/slug": "^5.0.9",
|
||||||
"@types/uuid": "~9.0.1",
|
"@types/uuid": "~9.0.1",
|
||||||
@ -110,19 +114,19 @@
|
|||||||
"eslint-config-standard": "^17.1.0",
|
"eslint-config-standard": "^17.1.0",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jest": "^29.1.0",
|
"eslint-plugin-jest": "^29.13.0",
|
||||||
"eslint-plugin-jsonc": "^2.21.0",
|
"eslint-plugin-jsonc": "^2.21.1",
|
||||||
"eslint-plugin-n": "^17.23.1",
|
"eslint-plugin-n": "^17.23.2",
|
||||||
"eslint-plugin-no-catch-all": "^1.1.0",
|
"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-promise": "^7.2.1",
|
||||||
"eslint-plugin-security": "^3.0.1",
|
"eslint-plugin-security": "^3.0.1",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "~3.1.11",
|
"nodemon": "~3.1.11",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.8.1",
|
||||||
"require-json5": "^1.3.0",
|
"require-json5": "^1.3.0",
|
||||||
"rosie": "^2.1.1",
|
"rosie": "^2.1.1",
|
||||||
"ts-jest": "^29.4.5",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.16",
|
"tsc-alias": "^1.8.16",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
@ -134,7 +138,8 @@
|
|||||||
"**/strip-ansi": "6.0.1",
|
"**/strip-ansi": "6.0.1",
|
||||||
"**/string-width": "4.2.0",
|
"**/string-width": "4.2.0",
|
||||||
"**/wrap-ansi": "7.0.0",
|
"**/wrap-ansi": "7.0.0",
|
||||||
"**/jwa": "^2.0.1"
|
"**/jwa": "^2.0.1",
|
||||||
|
"**/@types/express": "4.17.25"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.12.1"
|
"node": ">=20.12.1"
|
||||||
|
|||||||
@ -138,6 +138,9 @@ const options = {
|
|||||||
MAX_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_PINNED_POSTS))
|
MAX_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_PINNED_POSTS))
|
||||||
? 1
|
? 1
|
||||||
: Number(process.env.MAX_PINNED_POSTS),
|
: 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 = {
|
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()
|
||||||
|
})()
|
||||||
@ -58,6 +58,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
pinned: { type: 'boolean', default: null, valid: [null, true] },
|
pinned: { type: 'boolean', default: null, valid: [null, true] },
|
||||||
|
groupPinned: { type: 'boolean', default: null, valid: [null, true] },
|
||||||
postType: { type: 'string', default: 'Article', valid: ['Article', 'Event'] },
|
postType: { type: 'string', default: 'Article', valid: ['Article', 'Event'] },
|
||||||
observes: {
|
observes: {
|
||||||
type: 'relationship',
|
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
|
// 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
|
// 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 {
|
export default {
|
||||||
File: typeof Cypress !== 'undefined' ? require('./File') : require('./File').default,
|
Badge,
|
||||||
Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default,
|
Category,
|
||||||
Badge: typeof Cypress !== 'undefined' ? require('./Badge') : require('./Badge').default,
|
Comment,
|
||||||
User: typeof Cypress !== 'undefined' ? require('./User') : require('./User').default,
|
Donations,
|
||||||
Group: typeof Cypress !== 'undefined' ? require('./Group') : require('./Group').default,
|
EmailAddress,
|
||||||
EmailAddress:
|
File,
|
||||||
typeof Cypress !== 'undefined' ? require('./EmailAddress') : require('./EmailAddress').default,
|
Group,
|
||||||
UnverifiedEmailAddress:
|
Image,
|
||||||
typeof Cypress !== 'undefined'
|
InviteCode,
|
||||||
? require('./UnverifiedEmailAddress')
|
Location,
|
||||||
: require('./UnverifiedEmailAddress').default,
|
Migration,
|
||||||
SocialMedia:
|
Post,
|
||||||
typeof Cypress !== 'undefined' ? require('./SocialMedia') : require('./SocialMedia').default,
|
Report,
|
||||||
Post: typeof Cypress !== 'undefined' ? require('./Post') : require('./Post').default,
|
SocialMedia,
|
||||||
Comment: typeof Cypress !== 'undefined' ? require('./Comment') : require('./Comment').default,
|
Tag,
|
||||||
Category: typeof Cypress !== 'undefined' ? require('./Category') : require('./Category').default,
|
UnverifiedEmailAddress,
|
||||||
Tag: typeof Cypress !== 'undefined' ? require('./Tag') : require('./Tag').default,
|
User,
|
||||||
Location: typeof Cypress !== 'undefined' ? require('./Location') : require('./Location').default,
|
} as unknown as Record<string, Neode.SchemaObject>
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import Email from 'email-templates'
|
import Email from 'email-templates'
|
||||||
@ -94,8 +93,8 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
|||||||
: notification?.from?.title,
|
: notification?.from?.title,
|
||||||
postUrl: new URL(
|
postUrl: new URL(
|
||||||
notification?.from?.__typename === 'Comment'
|
notification?.from?.__typename === 'Comment'
|
||||||
? `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}`
|
? `/post/${encodeURIComponent(notification?.from?.post?.id)}/${encodeURIComponent(notification?.from?.post?.slug)}`
|
||||||
: `/post/${notification?.from?.id}/${notification?.from?.slug}`,
|
: `/post/${encodeURIComponent(notification?.from?.id)}/${encodeURIComponent(notification?.from?.slug)}`,
|
||||||
CONFIG.CLIENT_URI,
|
CONFIG.CLIENT_URI,
|
||||||
),
|
),
|
||||||
postAuthorName:
|
postAuthorName:
|
||||||
@ -106,7 +105,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
|||||||
notification?.from?.__typename === 'Comment'
|
notification?.from?.__typename === 'Comment'
|
||||||
? undefined
|
? undefined
|
||||||
: new URL(
|
: 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,
|
CONFIG.CLIENT_URI,
|
||||||
),
|
),
|
||||||
commenterName:
|
commenterName:
|
||||||
@ -116,14 +115,14 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
|||||||
commenterUrl:
|
commenterUrl:
|
||||||
notification?.from?.__typename === 'Comment'
|
notification?.from?.__typename === 'Comment'
|
||||||
? new URL(
|
? 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,
|
CONFIG.CLIENT_URI,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
commentUrl:
|
commentUrl:
|
||||||
notification?.from?.__typename === 'Comment'
|
notification?.from?.__typename === 'Comment'
|
||||||
? new URL(
|
? 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,
|
CONFIG.CLIENT_URI,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -132,7 +131,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
|||||||
groupUrl:
|
groupUrl:
|
||||||
notification?.from?.__typename === 'Group'
|
notification?.from?.__typename === 'Group'
|
||||||
? new URL(
|
? new URL(
|
||||||
`/groups/${notification?.from?.id}/${notification?.from?.slug}`,
|
`/groups/${encodeURIComponent(notification?.from?.id)}/${encodeURIComponent(notification?.from?.slug)}`,
|
||||||
CONFIG.CLIENT_URI,
|
CONFIG.CLIENT_URI,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -143,7 +142,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
|||||||
groupRelatedUserUrl:
|
groupRelatedUserUrl:
|
||||||
notification?.from?.__typename === 'Group'
|
notification?.from?.__typename === 'Group'
|
||||||
? new URL(
|
? new URL(
|
||||||
`/profile/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
|
`/profile/${encodeURIComponent(notification?.relatedUser?.id)}/${encodeURIComponent(notification?.relatedUser?.slug)}`,
|
||||||
CONFIG.CLIENT_URI,
|
CONFIG.CLIENT_URI,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -177,7 +176,10 @@ export const sendChatMessageMail = async (
|
|||||||
locale: recipientUser.locale,
|
locale: recipientUser.locale,
|
||||||
name: recipientUser.name,
|
name: recipientUser.name,
|
||||||
chattingUser: senderUser.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),
|
chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
|||||||
export const ChangeGroupMemberRole = gql`
|
export const ChangeGroupMemberRole = gql`
|
||||||
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
||||||
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
||||||
id
|
user {
|
||||||
name
|
id
|
||||||
slug
|
name
|
||||||
myRoleInGroup
|
slug
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
|||||||
export const GroupMembers = gql`
|
export const GroupMembers = gql`
|
||||||
query GroupMembers($id: ID!) {
|
query GroupMembers($id: ID!) {
|
||||||
GroupMembers(id: $id) {
|
GroupMembers(id: $id) {
|
||||||
id
|
user {
|
||||||
name
|
id
|
||||||
slug
|
name
|
||||||
myRoleInGroup
|
slug
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
|||||||
export const JoinGroup = gql`
|
export const JoinGroup = gql`
|
||||||
mutation ($groupId: ID!, $userId: ID!) {
|
mutation ($groupId: ID!, $userId: ID!) {
|
||||||
JoinGroup(groupId: $groupId, userId: $userId) {
|
JoinGroup(groupId: $groupId, userId: $userId) {
|
||||||
id
|
user {
|
||||||
name
|
id
|
||||||
slug
|
name
|
||||||
myRoleInGroup
|
slug
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
|||||||
export const LeaveGroup = gql`
|
export const LeaveGroup = gql`
|
||||||
mutation ($groupId: ID!, $userId: ID!) {
|
mutation ($groupId: ID!, $userId: ID!) {
|
||||||
LeaveGroup(groupId: $groupId, userId: $userId) {
|
LeaveGroup(groupId: $groupId, userId: $userId) {
|
||||||
id
|
user {
|
||||||
name
|
id
|
||||||
slug
|
name
|
||||||
myRoleInGroup
|
slug
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,10 +3,14 @@ import gql from 'graphql-tag'
|
|||||||
export const RemoveUserFromGroup = gql`
|
export const RemoveUserFromGroup = gql`
|
||||||
mutation ($groupId: ID!, $userId: ID!) {
|
mutation ($groupId: ID!, $userId: ID!) {
|
||||||
RemoveUserFromGroup(groupId: $groupId, userId: $userId) {
|
RemoveUserFromGroup(groupId: $groupId, userId: $userId) {
|
||||||
id
|
user {
|
||||||
name
|
id
|
||||||
slug
|
name
|
||||||
myRoleInGroup
|
slug
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
`
|
||||||
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
|
id
|
||||||
title
|
title
|
||||||
content
|
content
|
||||||
|
groupPinned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const reports = gql`
|
export const reports = gql`
|
||||||
query ($closed: Boolean) {
|
query (
|
||||||
reports(orderBy: createdAt_desc, closed: $closed) {
|
$orderBy: ReportOrdering
|
||||||
|
$reviewed: Boolean
|
||||||
|
$closed: Boolean
|
||||||
|
$first: Int
|
||||||
|
$offset: Int
|
||||||
|
) {
|
||||||
|
reports(
|
||||||
|
orderBy: $orderBy
|
||||||
|
reviewed: $reviewed
|
||||||
|
closed: $closed
|
||||||
|
first: $first
|
||||||
|
offset: $offset
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
|||||||
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'
|
import { GraphQLUpload } from 'graphql-upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -130,10 +130,13 @@ export const attachments = (config: S3Config) => {
|
|||||||
const { upload } = fileInput
|
const { upload } = fileInput
|
||||||
if (!upload) throw new UserInputError('Cannot find attachment for given resource')
|
if (!upload) throw new UserInputError('Cannot find attachment for given resource')
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const uploadFile = await upload
|
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 { name: fileName, ext } = path.parse(uploadFile.filename)
|
||||||
const uniqueFilename = `${uuid()}-${slug(fileName)}${ext}`
|
const uniqueFilename = `${uuid()}-${slug(fileName)}${ext}`
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
const url = await s3.uploadFile({
|
const url = await s3.uploadFile({
|
||||||
...uploadFile,
|
...uploadFile,
|
||||||
uniqueFilename,
|
uniqueFilename,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,20 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
|
||||||
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
import { getDriver, getNeode } from '@db/neo4j'
|
|
||||||
import { followUser } from '@graphql/queries/followUser'
|
import { followUser } from '@graphql/queries/followUser'
|
||||||
import { unfollowUser } from '@graphql/queries/unfollowUser'
|
import { unfollowUser } from '@graphql/queries/unfollowUser'
|
||||||
import { User } from '@graphql/queries/User'
|
import { User } from '@graphql/queries/User'
|
||||||
import createServer from '@src/server'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||||
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
const driver = getDriver()
|
let authenticatedUser: Context['user']
|
||||||
const neode = getNeode()
|
const context = () => ({ authenticatedUser })
|
||||||
|
let mutate: ApolloTestSetup['mutate']
|
||||||
let query
|
let query: ApolloTestSetup['query']
|
||||||
let mutate
|
let database: ApolloTestSetup['database']
|
||||||
let authenticatedUser
|
let server: ApolloTestSetup['server']
|
||||||
|
|
||||||
let user1
|
let user1
|
||||||
let user2
|
let user2
|
||||||
@ -24,26 +23,18 @@ let variables
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
|
const apolloSetup = createApolloTestSetup({ context })
|
||||||
const { server } = createServer({
|
mutate = apolloSetup.mutate
|
||||||
context: () => ({
|
query = apolloSetup.query
|
||||||
driver,
|
database = apolloSetup.database
|
||||||
neode,
|
server = apolloSetup.server
|
||||||
user: authenticatedUser,
|
|
||||||
cypherParams: {
|
|
||||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const testClient = createTestClient(server)
|
|
||||||
query = testClient.query
|
|
||||||
mutate = testClient.mutate
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
await driver.close()
|
void server.stop()
|
||||||
|
void database.driver.close()
|
||||||
|
database.neode.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -118,7 +109,7 @@ describe('follow', () => {
|
|||||||
mutation: followUser,
|
mutation: followUser,
|
||||||
variables,
|
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',
|
'MATCH (user:User {id: $id})-[relationship:FOLLOWS]->(followed:User) WHERE relationship.createdAt IS NOT NULL RETURN relationship',
|
||||||
{ id: 'u1' },
|
{ id: 'u1' },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -891,8 +891,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
JoinGroup: {
|
JoinGroup: {
|
||||||
id: 'owner-of-closed-group',
|
user: {
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-closed-group',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'usual',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -914,8 +918,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
JoinGroup: {
|
JoinGroup: {
|
||||||
id: 'current-user',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'current-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -939,8 +947,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
JoinGroup: {
|
JoinGroup: {
|
||||||
id: 'current-user',
|
user: {
|
||||||
myRoleInGroup: 'pending',
|
id: 'current-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'pending',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -962,8 +974,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
JoinGroup: {
|
JoinGroup: {
|
||||||
id: 'owner-of-closed-group',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-closed-group',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1001,8 +1017,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
JoinGroup: {
|
JoinGroup: {
|
||||||
id: 'owner-of-hidden-group',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-hidden-group',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1208,16 +1228,28 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1241,16 +1273,28 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1274,16 +1318,28 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1317,16 +1373,28 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'pending',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'pending',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1350,16 +1418,28 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'pending',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'pending',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1415,20 +1495,36 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'pending-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'pending',
|
id: 'pending-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'pending',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'admin',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'admin',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1452,20 +1548,36 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'pending-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'pending',
|
id: 'pending-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'pending',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'admin',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'admin',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1489,20 +1601,36 @@ describe('in mode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'pending-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'pending',
|
id: 'pending-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'pending',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'current-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'usual',
|
id: 'current-user',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'usual',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-closed-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'admin',
|
id: 'owner-of-closed-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'admin',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'owner-of-hidden-group',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-of-hidden-group',
|
||||||
|
}),
|
||||||
|
membership: expect.objectContaining({
|
||||||
|
role: 'owner',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -1600,8 +1728,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'usual-member-user',
|
user: {
|
||||||
myRoleInGroup: 'usual',
|
id: 'usual-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'usual',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1638,8 +1770,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'admin-member-user',
|
user: {
|
||||||
myRoleInGroup: 'admin',
|
id: 'admin-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1673,8 +1809,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'second-owner-member-user',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'second-owner-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1759,8 +1899,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'owner-member-user',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'owner-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1869,8 +2013,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'admin-member-user',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'admin-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2047,8 +2195,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'usual-member-user',
|
user: {
|
||||||
myRoleInGroup: 'admin',
|
id: 'usual-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2073,8 +2225,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'usual-member-user',
|
user: {
|
||||||
myRoleInGroup: 'usual',
|
id: 'usual-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'usual',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2234,8 +2390,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'pending-member-user',
|
user: {
|
||||||
myRoleInGroup: 'usual',
|
id: 'pending-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'usual',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2260,8 +2420,12 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
ChangeGroupMemberRole: {
|
ChangeGroupMemberRole: {
|
||||||
id: 'pending-member-user',
|
user: {
|
||||||
myRoleInGroup: 'pending',
|
id: 'pending-member-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'pending',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2413,7 +2577,7 @@ describe('in mode', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return result.data?.GroupMembers
|
return result.data?.GroupMembers
|
||||||
? !!result.data.GroupMembers.find((member) => member.id === userId)
|
? !!result.data.GroupMembers.find((member) => member.user.id === userId)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2440,8 +2604,10 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
LeaveGroup: {
|
LeaveGroup: {
|
||||||
id: 'pending-member-user',
|
user: {
|
||||||
myRoleInGroup: null,
|
id: 'pending-member-user',
|
||||||
|
},
|
||||||
|
membership: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2467,8 +2633,10 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
LeaveGroup: {
|
LeaveGroup: {
|
||||||
id: 'usual-member-user',
|
user: {
|
||||||
myRoleInGroup: null,
|
id: 'usual-member-user',
|
||||||
|
},
|
||||||
|
membership: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -2494,8 +2662,10 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
LeaveGroup: {
|
LeaveGroup: {
|
||||||
id: 'admin-member-user',
|
user: {
|
||||||
myRoleInGroup: null,
|
id: 'admin-member-user',
|
||||||
|
},
|
||||||
|
membership: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -3021,8 +3191,10 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
RemoveUserFromGroup: expect.objectContaining({
|
RemoveUserFromGroup: expect.objectContaining({
|
||||||
id: 'usual-member-user',
|
user: expect.objectContaining({
|
||||||
myRoleInGroup: null,
|
id: 'usual-member-user',
|
||||||
|
}),
|
||||||
|
membership: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -3093,8 +3265,10 @@ describe('in mode', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
RemoveUserFromGroup: expect.objectContaining({
|
RemoveUserFromGroup: expect.objectContaining({
|
||||||
id: 'usual-member-user',
|
user: {
|
||||||
myRoleInGroup: null,
|
id: 'usual-member-user',
|
||||||
|
},
|
||||||
|
membership: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
|
|||||||
@ -24,9 +24,6 @@ export default {
|
|||||||
Query: {
|
Query: {
|
||||||
Group: async (_object, params, context: Context, _resolveInfo) => {
|
Group: async (_object, params, context: Context, _resolveInfo) => {
|
||||||
const { isMember, id, slug, first, offset } = params
|
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 }
|
const matchParams = { id, slug }
|
||||||
removeUndefinedNullValuesFromObject(matchParams)
|
removeUndefinedNullValuesFromObject(matchParams)
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
@ -34,43 +31,22 @@ export default {
|
|||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
throw new Error('Missing authenticated user.')
|
throw new Error('Missing authenticated user.')
|
||||||
}
|
}
|
||||||
const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true)
|
const transactionResponse = await txc.run(
|
||||||
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}
|
|
||||||
`
|
`
|
||||||
} else {
|
MATCH (group:Group${convertObjectToCypherMapLiteral(matchParams, true)})
|
||||||
if (isMember === false) {
|
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||||
groupCypher = `
|
WITH group, membership
|
||||||
MATCH (group:Group${groupMatchParamsCypher})
|
${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||||
WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
|
${(isMember === false && "WHERE membership IS NULL AND (group.groupType IN ['public', 'closed'])") || ''}
|
||||||
WITH group
|
${(isMember === undefined && "WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||||
WHERE group.groupType IN ['public', 'closed']
|
RETURN group {.*, myRole: membership.role}
|
||||||
RETURN group {.*, myRole: NULL}
|
ORDER BY group.createdAt DESC
|
||||||
${orderBy}
|
${first !== undefined && offset !== undefined ? `SKIP ${offset} LIMIT ${first}` : ''}
|
||||||
${pagination}
|
`,
|
||||||
`
|
{
|
||||||
} else {
|
userId: context.user.id,
|
||||||
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,
|
|
||||||
})
|
|
||||||
return transactionResponse.records.map((record) => record.get('group'))
|
return transactionResponse.records.map((record) => record.get('group'))
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
@ -87,7 +63,7 @@ export default {
|
|||||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||||
const groupMemberCypher = `
|
const groupMemberCypher = `
|
||||||
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
|
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
|
||||||
RETURN user {.*, myRoleInGroup: membership.role}
|
RETURN user {.*}, membership {.*}
|
||||||
SKIP toInteger($offset) LIMIT toInteger($first)
|
SKIP toInteger($offset) LIMIT toInteger($first)
|
||||||
`
|
`
|
||||||
const transactionResponse = await txc.run(groupMemberCypher, {
|
const transactionResponse = await txc.run(groupMemberCypher, {
|
||||||
@ -95,7 +71,9 @@ export default {
|
|||||||
first,
|
first,
|
||||||
offset,
|
offset,
|
||||||
})
|
})
|
||||||
return transactionResponse.records.map((record) => record.get('user'))
|
return transactionResponse.records.map((record) => {
|
||||||
|
return { user: record.get('user'), membership: record.get('membership') }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
return await readTxResultPromise
|
return await readTxResultPromise
|
||||||
@ -297,8 +275,8 @@ export default {
|
|||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
const joinGroupCypher = `
|
const joinGroupCypher = `
|
||||||
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
|
MATCH (user:User {id: $userId}), (group:Group {id: $groupId})
|
||||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
MERGE (user)-[membership:MEMBER_OF]->(group)
|
||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
membership.createdAt = toString(datetime()),
|
membership.createdAt = toString(datetime()),
|
||||||
membership.updatedAt = null,
|
membership.updatedAt = null,
|
||||||
@ -307,14 +285,15 @@ export default {
|
|||||||
THEN 'usual'
|
THEN 'usual'
|
||||||
ELSE 'pending'
|
ELSE 'pending'
|
||||||
END
|
END
|
||||||
RETURN member {.*, myRoleInGroup: membership.role}
|
RETURN user {.*}, membership {.*}
|
||||||
`
|
`
|
||||||
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
||||||
const [member] = transactionResponse.records.map((record) => record.get('member'))
|
return transactionResponse.records.map((record) => {
|
||||||
return member
|
return { user: record.get('user'), membership: record.get('membership') }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
return await writeTxResultPromise
|
return (await writeTxResultPromise)[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
} finally {
|
} finally {
|
||||||
@ -361,7 +340,7 @@ export default {
|
|||||||
membership.updatedAt = toString(datetime()),
|
membership.updatedAt = toString(datetime()),
|
||||||
membership.role = $roleInGroup
|
membership.role = $roleInGroup
|
||||||
${postRestrictionCypher}
|
${postRestrictionCypher}
|
||||||
RETURN member {.*, myRoleInGroup: membership.role}
|
RETURN member {.*} as user, membership {.*}
|
||||||
`
|
`
|
||||||
|
|
||||||
const transactionResponse = await transaction.run(joinGroupCypher, {
|
const transactionResponse = await transaction.run(joinGroupCypher, {
|
||||||
@ -369,7 +348,9 @@ export default {
|
|||||||
userId,
|
userId,
|
||||||
roleInGroup,
|
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
|
return member
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
@ -460,6 +441,23 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Group: {
|
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) => {
|
inviteCodes: async (parent, _args, context: Context, _resolveInfo) => {
|
||||||
if (!parent.id) {
|
if (!parent.id) {
|
||||||
throw new Error('Can not identify selected Group!')
|
throw new Error('Can not identify selected Group!')
|
||||||
@ -478,6 +476,18 @@ export default {
|
|||||||
})
|
})
|
||||||
).records.map((r) => r.get('inviteCodes'))
|
).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', {
|
...Resolver('Group', {
|
||||||
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
||||||
hasMany: {
|
hasMany: {
|
||||||
@ -523,14 +533,16 @@ const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId)
|
|||||||
WITH user, collect(p) AS posts
|
WITH user, collect(p) AS posts
|
||||||
FOREACH (post IN posts |
|
FOREACH (post IN posts |
|
||||||
MERGE (user)-[:CANNOT_SEE]->(post))
|
MERGE (user)-[:CANNOT_SEE]->(post))
|
||||||
RETURN user {.*, myRoleInGroup: NULL}
|
RETURN user {.*}, NULL as membership
|
||||||
`
|
`
|
||||||
|
|
||||||
const transactionResponse = await transaction.run(removeUserFromGroupCypher, {
|
const transactionResponse = await transaction.run(removeUserFromGroupCypher, {
|
||||||
groupId,
|
groupId,
|
||||||
userId,
|
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
|
return user
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { getMutedUsers } from '@graphql/resolvers/users'
|
|||||||
|
|
||||||
export const filterForMutedUsers = async (params, context) => {
|
export const filterForMutedUsers = async (params, context) => {
|
||||||
if (!context.user) return params
|
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 [mutedUsers] = await Promise.all([getMutedUsers(context)])
|
||||||
const mutedUsersIds = [...mutedUsers.map((user) => user.id)]
|
const mutedUsersIds = [...mutedUsers.map((user) => user.id)]
|
||||||
if (!mutedUsersIds.length) return params
|
if (!mutedUsersIds.length) return params
|
||||||
|
|||||||
@ -84,9 +84,12 @@ export const images = (config: S3Config) => {
|
|||||||
|
|
||||||
const uploadImageFile = async (uploadPromise: Promise<FileUpload> | undefined) => {
|
const uploadImageFile = async (uploadPromise: Promise<FileUpload> | undefined) => {
|
||||||
if (!uploadPromise) return undefined
|
if (!uploadPromise) return undefined
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const upload = await uploadPromise
|
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 { name, ext } = path.parse(upload.filename)
|
||||||
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
return await s3.uploadFile({ ...upload, uniqueFilename })
|
return await s3.uploadFile({ ...upload, uniqueFilename })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1089,16 +1089,24 @@ describe('redeemInviteCode', () => {
|
|||||||
data: {
|
data: {
|
||||||
GroupMembers: expect.arrayContaining([
|
GroupMembers: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
id: 'inviting-user',
|
user: {
|
||||||
myRoleInGroup: 'owner',
|
id: 'inviting-user',
|
||||||
name: 'Inviting User',
|
name: 'Inviting User',
|
||||||
slug: 'inviting-user',
|
slug: 'inviting-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'other-user',
|
user: {
|
||||||
myRoleInGroup: 'pending',
|
id: 'other-user',
|
||||||
name: 'Other User',
|
name: 'Other User',
|
||||||
slug: 'other-user',
|
slug: 'other-user',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: 'pending',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,41 +1,34 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
|
||||||
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
import { getNeode, getDriver } from '@db/neo4j'
|
|
||||||
import { UpdateUser } from '@graphql/queries/UpdateUser'
|
import { UpdateUser } from '@graphql/queries/UpdateUser'
|
||||||
import { User } from '@graphql/queries/User'
|
import { User } from '@graphql/queries/User'
|
||||||
import createServer from '@src/server'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||||
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
let query, mutate, authenticatedUser
|
let authenticatedUser: Context['user']
|
||||||
|
const context = () => ({ authenticatedUser })
|
||||||
const driver = getDriver()
|
let mutate: ApolloTestSetup['mutate']
|
||||||
const neode = getNeode()
|
let query: ApolloTestSetup['query']
|
||||||
|
let database: ApolloTestSetup['database']
|
||||||
|
let server: ApolloTestSetup['server']
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
|
const apolloSetup = createApolloTestSetup({ context })
|
||||||
const { server } = createServer({
|
mutate = apolloSetup.mutate
|
||||||
context: () => {
|
query = apolloSetup.query
|
||||||
return {
|
database = apolloSetup.database
|
||||||
driver,
|
server = apolloSetup.server
|
||||||
neode,
|
|
||||||
user: authenticatedUser,
|
|
||||||
cypherParams: {
|
|
||||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
query = createTestClient(server).query
|
|
||||||
mutate = createTestClient(server).mutate
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDatabase()
|
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
|
// 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
|
||||||
|
|||||||
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 }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -29,6 +29,20 @@ const maintainPinnedPosts = (params) => {
|
|||||||
return 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) => {
|
const filterEventDates = (params) => {
|
||||||
if (params.filter?.eventStart_gte) {
|
if (params.filter?.eventStart_gte) {
|
||||||
const date = params.filter.eventStart_gte
|
const date = params.filter.eventStart_gte
|
||||||
@ -52,6 +66,7 @@ export default {
|
|||||||
params = await filterPostsOfMyGroups(params, context)
|
params = await filterPostsOfMyGroups(params, context)
|
||||||
params = await filterInvisiblePosts(params, context)
|
params = await filterInvisiblePosts(params, context)
|
||||||
params = await filterForMutedUsers(params, context)
|
params = await filterForMutedUsers(params, context)
|
||||||
|
params = await maintainGroupPinnedPosts(params)
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
},
|
},
|
||||||
PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => {
|
PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => {
|
||||||
@ -154,7 +169,7 @@ export default {
|
|||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
const categoriesCypher =
|
const categoriesCypher =
|
||||||
config.CATEGORIES_ACTIVE && categoryIds
|
config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > 0
|
||||||
? `WITH post
|
? `WITH post
|
||||||
UNWIND $categoryIds AS categoryId
|
UNWIND $categoryIds AS categoryId
|
||||||
MATCH (category:Category {id: categoryId})
|
MATCH (category:Category {id: categoryId})
|
||||||
@ -453,6 +468,68 @@ export default {
|
|||||||
}
|
}
|
||||||
return unpinnedPost
|
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) => {
|
markTeaserAsViewed: async (_parent, params, context, _resolveInfo) => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
@ -550,6 +627,7 @@ export default {
|
|||||||
'language',
|
'language',
|
||||||
'pinnedAt',
|
'pinnedAt',
|
||||||
'pinned',
|
'pinned',
|
||||||
|
'groupPinned',
|
||||||
'eventVenue',
|
'eventVenue',
|
||||||
'eventLocation',
|
'eventLocation',
|
||||||
'eventLocationName',
|
'eventLocationName',
|
||||||
@ -589,6 +667,21 @@ export default {
|
|||||||
'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1',
|
'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) => {
|
relatedContributions: async (parent, _params, context, _resolveInfo) => {
|
||||||
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||||
const { id } = parent
|
const { id } = parent
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import createServer from '@src/server'
|
|||||||
const instance = getNeode()
|
const instance = getNeode()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|
||||||
describe('file a report on a resource', () => {
|
describe('reports', () => {
|
||||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
const variables = {
|
const variables = {
|
||||||
@ -620,32 +620,31 @@ describe('file a report on a resource', () => {
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
authenticatedUser = await currentUser.toJson()
|
authenticatedUser = await currentUser.toJson()
|
||||||
await Promise.all([
|
// Sequential to ensure distinct createdAt values for orderBy tests
|
||||||
mutate({
|
await mutate({
|
||||||
mutation: fileReport,
|
mutation: fileReport,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-post-1',
|
resourceId: 'abusive-post-1',
|
||||||
reasonCategory: 'other',
|
reasonCategory: 'other',
|
||||||
reasonDescription: 'This comment is bigoted',
|
reasonDescription: 'This post is bigoted',
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
mutate({
|
await mutate({
|
||||||
mutation: fileReport,
|
mutation: fileReport,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-comment-1',
|
resourceId: 'abusive-comment-1',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonCategory: 'discrimination_etc',
|
||||||
reasonDescription: 'This post is bigoted',
|
reasonDescription: 'This comment is bigoted',
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
mutate({
|
await mutate({
|
||||||
mutation: fileReport,
|
mutation: fileReport,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-user-1',
|
resourceId: 'abusive-user-1',
|
||||||
reasonCategory: 'doxing',
|
reasonCategory: 'doxing',
|
||||||
reasonDescription: 'This user is harassing me with bigoted remarks',
|
reasonDescription: 'This user is harassing me with bigoted remarks',
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
])
|
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -660,82 +659,250 @@ describe('file a report on a resource', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
it('role "user" gets no reports', async () => {
|
describe('as user', () => {
|
||||||
authenticatedUser = await currentUser.toJson()
|
beforeEach(async () => {
|
||||||
await expect(query({ query: reports })).resolves.toMatchObject({
|
authenticatedUser = await currentUser.toJson()
|
||||||
data: { reports: null },
|
})
|
||||||
errors: [{ message: 'Not Authorized!' }],
|
|
||||||
|
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 () => {
|
describe('as moderator', () => {
|
||||||
const expected = {
|
beforeEach(async () => {
|
||||||
reports: expect.arrayContaining([
|
authenticatedUser = await moderator.toJson()
|
||||||
expect.objectContaining({
|
})
|
||||||
id: expect.any(String),
|
|
||||||
createdAt: expect.any(String),
|
it('gets reports', async () => {
|
||||||
updatedAt: expect.any(String),
|
const expected = {
|
||||||
closed: false,
|
reports: expect.arrayContaining([
|
||||||
resource: {
|
expect.objectContaining({
|
||||||
__typename: 'User',
|
id: expect.any(String),
|
||||||
id: 'abusive-user-1',
|
createdAt: expect.any(String),
|
||||||
},
|
updatedAt: expect.any(String),
|
||||||
filed: expect.arrayContaining([
|
closed: false,
|
||||||
expect.objectContaining({
|
resource: {
|
||||||
submitter: expect.objectContaining({
|
__typename: 'User',
|
||||||
id: 'current-user-id',
|
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),
|
||||||
expect.objectContaining({
|
closed: false,
|
||||||
id: expect.any(String),
|
resource: {
|
||||||
createdAt: expect.any(String),
|
__typename: 'Post',
|
||||||
updatedAt: expect.any(String),
|
id: 'abusive-post-1',
|
||||||
closed: false,
|
},
|
||||||
resource: {
|
filed: expect.arrayContaining([
|
||||||
__typename: 'Post',
|
expect.objectContaining({
|
||||||
id: 'abusive-post-1',
|
submitter: expect.objectContaining({
|
||||||
},
|
id: 'current-user-id',
|
||||||
filed: expect.arrayContaining([
|
}),
|
||||||
expect.objectContaining({
|
createdAt: expect.any(String),
|
||||||
submitter: expect.objectContaining({
|
reasonCategory: 'other',
|
||||||
id: 'current-user-id',
|
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),
|
||||||
expect.objectContaining({
|
closed: false,
|
||||||
id: expect.any(String),
|
resource: {
|
||||||
createdAt: expect.any(String),
|
__typename: 'Comment',
|
||||||
updatedAt: expect.any(String),
|
id: 'abusive-comment-1',
|
||||||
closed: false,
|
},
|
||||||
resource: {
|
filed: expect.arrayContaining([
|
||||||
__typename: 'Comment',
|
expect.objectContaining({
|
||||||
id: 'abusive-comment-1',
|
submitter: expect.objectContaining({
|
||||||
},
|
id: 'current-user-id',
|
||||||
filed: expect.arrayContaining([
|
}),
|
||||||
expect.objectContaining({
|
createdAt: expect.any(String),
|
||||||
submitter: expect.objectContaining({
|
reasonCategory: 'discrimination_etc',
|
||||||
id: 'current-user-id',
|
reasonDescription: 'This comment is bigoted',
|
||||||
}),
|
}),
|
||||||
createdAt: expect.any(String),
|
]),
|
||||||
reasonCategory: 'discrimination_etc',
|
}),
|
||||||
reasonDescription: 'This post is bigoted',
|
]),
|
||||||
}),
|
}
|
||||||
]),
|
const { data } = await query({ query: reports })
|
||||||
}),
|
expect(data).toEqual(expected)
|
||||||
]),
|
})
|
||||||
}
|
|
||||||
authenticatedUser = await moderator.toJson()
|
describe('orderBy', () => {
|
||||||
const { data } = await query({ query: reports })
|
it('createdAt_asc returns reports in ascending order', async () => {
|
||||||
expect(data).toEqual(expected)
|
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) => {
|
reports: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { driver } = context
|
const { driver } = context
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
let orderByClause, filterClause
|
let orderByClause
|
||||||
|
const filterClauses: string[] = []
|
||||||
switch (params.orderBy) {
|
switch (params.orderBy) {
|
||||||
case 'createdAt_asc':
|
case 'createdAt_asc':
|
||||||
orderByClause = 'ORDER BY report.createdAt ASC'
|
orderByClause = 'ORDER BY report.createdAt ASC'
|
||||||
@ -59,26 +60,24 @@ export default {
|
|||||||
|
|
||||||
switch (params.reviewed) {
|
switch (params.reviewed) {
|
||||||
case true:
|
case true:
|
||||||
filterClause = 'AND ((report)<-[:REVIEWED]-(:User))'
|
filterClauses.push('AND ((report)<-[:REVIEWED]-(:User))')
|
||||||
break
|
break
|
||||||
case false:
|
case false:
|
||||||
filterClause = 'AND NOT ((report)<-[:REVIEWED]-(:User))'
|
filterClauses.push('AND NOT ((report)<-[:REVIEWED]-(:User))')
|
||||||
break
|
break
|
||||||
default:
|
|
||||||
filterClause = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (params.closed) {
|
switch (params.closed) {
|
||||||
case true:
|
case true:
|
||||||
filterClause = 'AND report.closed = true'
|
filterClauses.push('AND report.closed = true')
|
||||||
break
|
break
|
||||||
case false:
|
case false:
|
||||||
filterClause = 'AND report.closed = false'
|
filterClauses.push('AND report.closed = false')
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterClause = filterClauses.join(' ')
|
||||||
|
|
||||||
const offset =
|
const offset =
|
||||||
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
||||||
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
||||||
@ -114,7 +113,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Report: {
|
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
|
if (typeof parent.filed !== 'undefined') return parent.filed
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const { id } = parent
|
const { id } = parent
|
||||||
@ -146,9 +146,9 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return filed
|
return filed
|
||||||
},
|
}, */
|
||||||
reviewed: async (parent, _params, context, _resolveInfo) => {
|
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 session = context.driver.session()
|
||||||
const { id } = parent
|
const { id } = parent
|
||||||
let reviewed
|
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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,12 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/require-await */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* 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-return */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { AuthenticationError } from 'apollo-server'
|
import { AuthenticationError } from 'apollo-server'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
|
||||||
|
|
||||||
import { getNeode } from '@db/neo4j'
|
import { getNeode } from '@db/neo4j'
|
||||||
import { encode } from '@jwt/encode'
|
import { encode } from '@jwt/encode'
|
||||||
@ -18,8 +15,21 @@ const neode = getNeode()
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
currentUser: async (object, params, context, resolveInfo) =>
|
currentUser: async (_object, _params, context: Context, _resolveInfo) => {
|
||||||
neo4jgraphql(object, { id: context.user.id }, 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: {
|
Mutation: {
|
||||||
login: async (_, { email, password }, context: Context) => {
|
login: async (_, { email, password }, context: Context) => {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/require-await */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
@ -458,6 +457,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
User: {
|
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) => {
|
inviteCodes: async (_parent, _args, context: Context, _resolveInfo) => {
|
||||||
return (
|
return (
|
||||||
await context.database.query({
|
await context.database.query({
|
||||||
@ -471,7 +482,7 @@ export default {
|
|||||||
})
|
})
|
||||||
).records.map((record) => record.get('inviteCodes'))
|
).records.map((record) => record.get('inviteCodes'))
|
||||||
},
|
},
|
||||||
emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => {
|
emailNotificationSettings: (parent, _params, _context, _resolveInfo) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'post',
|
type: 'post',
|
||||||
@ -633,7 +644,6 @@ export default {
|
|||||||
'allowEmbedIframes',
|
'allowEmbedIframes',
|
||||||
'showShoutsPublicly',
|
'showShoutsPublicly',
|
||||||
'locale',
|
'locale',
|
||||||
'activeCategories',
|
|
||||||
],
|
],
|
||||||
boolean: {
|
boolean: {
|
||||||
followedByCurrentUser:
|
followedByCurrentUser:
|
||||||
|
|||||||
@ -245,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', () => {
|
describe('from the perspective of the blocked user', () => {
|
||||||
|
|||||||
@ -241,6 +241,79 @@ describe('muteUser', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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: [
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
||||||
|
|
||||||
import typeDefs from '@graphql/types/index'
|
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!
|
isPostObservedByMe: Boolean!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
||||||
)
|
)
|
||||||
postObservingUsersCount: Int!
|
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!
|
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!
|
shoutedCount: Int!
|
||||||
@cypher(
|
@cypher(
|
||||||
@ -77,16 +81,7 @@ type Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateComment(
|
CreateComment(id: ID, postId: ID!, content: String!, contentExcerpt: String): Comment
|
||||||
id: ID
|
UpdateComment(id: ID!, content: String!, contentExcerpt: String): Comment
|
||||||
postId: ID!
|
|
||||||
content: String!
|
|
||||||
contentExcerpt: String
|
|
||||||
): Comment
|
|
||||||
UpdateComment(
|
|
||||||
id: ID!
|
|
||||||
content: String!
|
|
||||||
contentExcerpt: String
|
|
||||||
): Comment
|
|
||||||
DeleteComment(id: ID!): Comment
|
DeleteComment(id: ID!): Comment
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,11 +9,7 @@ type Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
Signup(
|
Signup(email: String!, locale: String!, inviteCode: String = null): EmailAddress
|
||||||
email: String!
|
|
||||||
locale: String!
|
|
||||||
inviteCode: String = null
|
|
||||||
): EmailAddress
|
|
||||||
SignupVerification(
|
SignupVerification(
|
||||||
nonce: String!
|
nonce: String!
|
||||||
email: String!
|
email: String!
|
||||||
@ -27,8 +23,5 @@ type Mutation {
|
|||||||
locationName: String = null
|
locationName: String = null
|
||||||
): User
|
): User
|
||||||
AddEmailAddress(email: String!): EmailAddress
|
AddEmailAddress(email: String!): EmailAddress
|
||||||
VerifyEmailAddress(
|
VerifyEmailAddress(nonce: String!, email: String!): EmailAddress
|
||||||
nonce: String!
|
|
||||||
email: String!
|
|
||||||
): EmailAddress
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ type FILED {
|
|||||||
submitter: User
|
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 {
|
enum ReasonCategory {
|
||||||
other
|
other
|
||||||
discrimination_etc
|
discrimination_etc
|
||||||
@ -26,5 +26,9 @@ type FiledReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): FiledReport
|
fileReport(
|
||||||
|
resourceId: ID!
|
||||||
|
reasonCategory: ReasonCategory!
|
||||||
|
reasonDescription: String!
|
||||||
|
): FiledReport
|
||||||
}
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
type File {
|
type File {
|
||||||
url: ID!,
|
url: ID!
|
||||||
name: String,
|
name: String
|
||||||
#size: Int,
|
type: String
|
||||||
type: String,
|
# size: Int
|
||||||
#audio: Boolean,
|
# audio: Boolean
|
||||||
#duration: Float,
|
# duration: Float
|
||||||
#preview: String,
|
# preview: String
|
||||||
#progress: Int,
|
# progress: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input FileInput {
|
input FileInput {
|
||||||
upload: Upload,
|
upload: Upload
|
||||||
name: String,
|
name: String
|
||||||
type: String,
|
type: String
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
enum _GroupOrdering {
|
# enum _GroupOrdering {
|
||||||
id_asc
|
# id_asc
|
||||||
id_desc
|
# id_desc
|
||||||
name_asc
|
# name_asc
|
||||||
name_desc
|
# name_desc
|
||||||
slug_asc
|
# slug_asc
|
||||||
slug_desc
|
# slug_desc
|
||||||
locationName_asc
|
# locationName_asc
|
||||||
locationName_desc
|
# locationName_desc
|
||||||
about_asc
|
# about_asc
|
||||||
about_desc
|
# about_desc
|
||||||
createdAt_asc
|
# createdAt_asc
|
||||||
createdAt_desc
|
# createdAt_desc
|
||||||
updatedAt_asc
|
# updatedAt_asc
|
||||||
updatedAt_desc
|
# updatedAt_desc
|
||||||
}
|
# }
|
||||||
|
|
||||||
type Group {
|
type Group {
|
||||||
id: ID!
|
id: ID!
|
||||||
@ -38,18 +38,27 @@ type Group {
|
|||||||
|
|
||||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
membersCount: Int! @cypher(statement: "MATCH (this)<-[:MEMBER_OF]-(r:User) RETURN COUNT(DISTINCT r)")
|
membersCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[:MEMBER_OF]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
myRole: GroupMemberRole # if 'null' then the current user is no member
|
myRole: GroupMemberRole # if 'null' then the current user is no member
|
||||||
|
|
||||||
posts: [Post] @relation(name: "IN", direction: "IN")
|
posts: [Post] @relation(name: "IN", direction: "IN")
|
||||||
|
|
||||||
isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )")
|
isMutedByMe: Boolean!
|
||||||
|
@cypher(
|
||||||
|
statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )"
|
||||||
|
)
|
||||||
|
|
||||||
"inviteCodes to this group the current user has generated"
|
"inviteCodes to this group the current user has generated"
|
||||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||||
|
|
||||||
|
currentlyPinnedPostsCount: Int! @neo4j_ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GroupMember {
|
||||||
|
user: User
|
||||||
|
membership: MEMBER_OF
|
||||||
|
}
|
||||||
|
|
||||||
input _GroupFilter {
|
input _GroupFilter {
|
||||||
AND: [_GroupFilter!]
|
AND: [_GroupFilter!]
|
||||||
@ -74,17 +83,13 @@ type Query {
|
|||||||
slug: String
|
slug: String
|
||||||
first: Int
|
first: Int
|
||||||
offset: Int
|
offset: Int
|
||||||
# orderBy: [_GroupOrdering] # not implemented yet
|
|
||||||
# filter: _GroupFilter # not implemented yet
|
|
||||||
): [Group]
|
): [Group]
|
||||||
|
# orderBy: [_GroupOrdering] # not implemented yet
|
||||||
|
# filter: _GroupFilter # not implemented yet
|
||||||
|
|
||||||
GroupMembers(
|
GroupMembers(id: ID!, first: Int, offset: Int): [GroupMember]
|
||||||
id: ID!
|
# orderBy: [_UserOrdering] # not implemented yet
|
||||||
first: Int
|
# filter: _UserFilter # not implemented yet
|
||||||
offset: Int
|
|
||||||
# orderBy: [_UserOrdering] # not implemented yet
|
|
||||||
# filter: _UserFilter # not implemented yet
|
|
||||||
): [User]
|
|
||||||
|
|
||||||
GroupCount(isMember: Boolean): Int
|
GroupCount(isMember: Boolean): Int
|
||||||
|
|
||||||
@ -105,7 +110,9 @@ type Mutation {
|
|||||||
groupType: GroupType!
|
groupType: GroupType!
|
||||||
actionRadius: GroupActionRadius!
|
actionRadius: GroupActionRadius!
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
|
|
||||||
# avatar: ImageInput # a group can not be created with an avatar
|
# avatar: ImageInput # a group can not be created with an avatar
|
||||||
|
|
||||||
locationName: String # empty string '' sets it to null
|
locationName: String # empty string '' sets it to null
|
||||||
): Group
|
): Group
|
||||||
|
|
||||||
@ -115,7 +122,9 @@ type Mutation {
|
|||||||
slug: String
|
slug: String
|
||||||
about: String
|
about: String
|
||||||
description: String
|
description: String
|
||||||
|
|
||||||
# groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden
|
# groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden
|
||||||
|
|
||||||
actionRadius: GroupActionRadius
|
actionRadius: GroupActionRadius
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
avatar: ImageInput # test this as result
|
avatar: ImageInput # test this as result
|
||||||
@ -124,26 +133,13 @@ type Mutation {
|
|||||||
|
|
||||||
# DeleteGroup(id: ID!): Group
|
# DeleteGroup(id: ID!): Group
|
||||||
|
|
||||||
JoinGroup(
|
JoinGroup(groupId: ID!, userId: ID!): GroupMember
|
||||||
groupId: ID!
|
|
||||||
userId: ID!
|
|
||||||
): User
|
|
||||||
|
|
||||||
LeaveGroup(
|
LeaveGroup(groupId: ID!, userId: ID!): GroupMember
|
||||||
groupId: ID!
|
|
||||||
userId: ID!
|
|
||||||
): User
|
|
||||||
|
|
||||||
ChangeGroupMemberRole(
|
ChangeGroupMemberRole(groupId: ID!, userId: ID!, roleInGroup: GroupMemberRole!): GroupMember
|
||||||
groupId: ID!
|
|
||||||
userId: ID!
|
|
||||||
roleInGroup: GroupMemberRole!
|
|
||||||
): User
|
|
||||||
|
|
||||||
RemoveUserFromGroup(
|
RemoveUserFromGroup(groupId: ID!, userId: ID!): GroupMember
|
||||||
groupId: ID!
|
|
||||||
userId: ID!
|
|
||||||
): User
|
|
||||||
|
|
||||||
muteGroup(groupId: ID!): Group
|
muteGroup(groupId: ID!): Group
|
||||||
unmuteGroup(groupId: ID!): Group
|
unmuteGroup(groupId: ID!): Group
|
||||||
|
|||||||
@ -1,21 +1,23 @@
|
|||||||
type Image {
|
type Image {
|
||||||
url: ID!,
|
url: ID!
|
||||||
transform(width: Int, height: Int): String
|
transform(width: Int, height: Int): String
|
||||||
|
|
||||||
# urlW34: String,
|
# urlW34: String,
|
||||||
# urlW160: String,
|
# urlW160: String,
|
||||||
# urlW320: String,
|
# urlW320: String,
|
||||||
# urlW640: String,
|
# urlW640: String,
|
||||||
# urlW1024: String,
|
# urlW1024: String,
|
||||||
alt: String,
|
|
||||||
sensitive: Boolean,
|
alt: String
|
||||||
aspectRatio: Float,
|
sensitive: Boolean
|
||||||
type: String,
|
aspectRatio: Float
|
||||||
|
type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input ImageInput {
|
input ImageInput {
|
||||||
alt: String,
|
alt: String
|
||||||
upload: Upload,
|
upload: Upload
|
||||||
sensitive: Boolean,
|
sensitive: Boolean
|
||||||
aspectRatio: Float,
|
aspectRatio: Float
|
||||||
type: String,
|
type: String
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,11 @@ type Query {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
generatePersonalInviteCode(expiresAt: String = null, comment: String = null): InviteCode!
|
generatePersonalInviteCode(expiresAt: String = null, comment: String = null): InviteCode!
|
||||||
generateGroupInviteCode(groupId: ID!, expiresAt: String = null, comment: String = null): InviteCode!
|
generateGroupInviteCode(
|
||||||
|
groupId: ID!
|
||||||
|
expiresAt: String = null
|
||||||
|
comment: String = null
|
||||||
|
): InviteCode!
|
||||||
invalidateInviteCode(code: String!): InviteCode
|
invalidateInviteCode(code: String!): InviteCode
|
||||||
redeemInviteCode(code: String!): Boolean!
|
redeemInviteCode(code: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type Location {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# This is not smart - we need one location for everything - use the same type everywhere!
|
# This is not smart - we need one location for everything - use the same type everywhere!
|
||||||
|
|
||||||
type LocationMapBox {
|
type LocationMapBox {
|
||||||
id: ID!
|
id: ID!
|
||||||
place_name: String!
|
place_name: String!
|
||||||
|
|||||||
@ -19,8 +19,11 @@ type Message {
|
|||||||
|
|
||||||
senderId: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.id")
|
senderId: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.id")
|
||||||
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
|
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
|
||||||
avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url")
|
avatar: String
|
||||||
date: String! @cypher(statement: "RETURN this.createdAt")
|
@cypher(
|
||||||
|
statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url"
|
||||||
|
)
|
||||||
|
date: String! @cypher(statement: "RETURN this.createdAt")
|
||||||
|
|
||||||
saved: Boolean
|
saved: Boolean
|
||||||
distributed: Boolean
|
distributed: Boolean
|
||||||
@ -29,22 +32,13 @@ type Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateMessage(
|
CreateMessage(roomId: ID!, content: String, files: [FileInput]): Message
|
||||||
roomId: ID!
|
|
||||||
content: String
|
|
||||||
files: [FileInput]
|
|
||||||
): Message
|
|
||||||
|
|
||||||
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Message(
|
Message(roomId: ID!, first: Int, offset: Int, orderBy: [_MessageOrdering]): [Message]
|
||||||
roomId: ID!,
|
|
||||||
first: Int
|
|
||||||
offset: Int
|
|
||||||
orderBy: [_MessageOrdering]
|
|
||||||
): [Message]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
input _CategoryFilter {
|
||||||
|
AND: [_CategoryFilter!]
|
||||||
|
OR: [_CategoryFilter!]
|
||||||
|
id_in: [ID!]
|
||||||
|
}
|
||||||
input _PostFilter {
|
input _PostFilter {
|
||||||
AND: [_PostFilter!]
|
AND: [_PostFilter!]
|
||||||
OR: [_PostFilter!]
|
OR: [_PostFilter!]
|
||||||
@ -49,6 +54,7 @@ input _PostFilter {
|
|||||||
language_in: [String!]
|
language_in: [String!]
|
||||||
language_not_in: [String!]
|
language_not_in: [String!]
|
||||||
pinned: Boolean # required for `maintainPinnedPost`
|
pinned: Boolean # required for `maintainPinnedPost`
|
||||||
|
groupPinned: Boolean # required for `maintainGroupPinnedPost`
|
||||||
tags: _TagFilter
|
tags: _TagFilter
|
||||||
tags_not: _TagFilter
|
tags_not: _TagFilter
|
||||||
tags_in: [_TagFilter!]
|
tags_in: [_TagFilter!]
|
||||||
@ -111,9 +117,10 @@ enum _PostOrdering {
|
|||||||
pinned_desc
|
pinned_desc
|
||||||
eventStart_asc
|
eventStart_asc
|
||||||
eventStart_desc
|
eventStart_desc
|
||||||
|
groupPinned_asc
|
||||||
|
groupPinned_desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Post {
|
type Post {
|
||||||
id: ID!
|
id: ID!
|
||||||
activityId: String
|
activityId: String
|
||||||
@ -128,14 +135,16 @@ type Post {
|
|||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
pinned: Boolean
|
pinned: Boolean
|
||||||
|
groupPinned: Boolean
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
sortDate: String
|
sortDate: String
|
||||||
language: String
|
language: String
|
||||||
pinnedAt: String @cypher(
|
pinnedAt: String
|
||||||
|
@cypher(
|
||||||
statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt"
|
statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt"
|
||||||
)
|
)
|
||||||
pinnedBy: User @relation(name:"PINNED", direction: "IN")
|
pinnedBy: User @relation(name: "PINNED", direction: "IN")
|
||||||
relatedContributions: [Post]!
|
relatedContributions: [Post]!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
@ -160,7 +169,7 @@ type Post {
|
|||||||
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
|
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Has the currently logged in user shouted that post?
|
"Has the currently logged in user shouted that post?"
|
||||||
shoutedByCurrentUser: Boolean!
|
shoutedByCurrentUser: Boolean!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
|
statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
|
||||||
@ -180,8 +189,7 @@ type Post {
|
|||||||
|
|
||||||
group: Group @relation(name: "IN", direction: "OUT")
|
group: Group @relation(name: "IN", direction: "OUT")
|
||||||
|
|
||||||
postType: [PostType]
|
postType: [PostType] @cypher(statement: "RETURN [l IN labels(this) WHERE NOT l = 'Post']")
|
||||||
@cypher(statement: "RETURN [l IN labels(this) WHERE NOT l = 'Post']")
|
|
||||||
|
|
||||||
eventLocationName: String
|
eventLocationName: String
|
||||||
eventLocation: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
eventLocation: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||||
@ -193,9 +201,11 @@ type Post {
|
|||||||
isObservedByMe: Boolean!
|
isObservedByMe: Boolean!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: "MATCH (this)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
statement: "MATCH (this)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
||||||
)
|
)
|
||||||
observingUsersCount: Int!
|
observingUsersCount: Int!
|
||||||
@cypher(statement: "MATCH (this)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.deleted = true AND NOT u.disabled = true RETURN COUNT(DISTINCT u)")
|
@cypher(
|
||||||
|
statement: "MATCH (this)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.deleted = true AND NOT u.disabled = true RETURN COUNT(DISTINCT u)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
input _PostInput {
|
input _PostInput {
|
||||||
@ -216,7 +226,7 @@ type Mutation {
|
|||||||
title: String!
|
title: String!
|
||||||
slug: String
|
slug: String
|
||||||
content: String!
|
content: String!
|
||||||
image: ImageInput,
|
image: ImageInput
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
@ -231,7 +241,7 @@ type Mutation {
|
|||||||
slug: String
|
slug: String
|
||||||
content: String!
|
content: String!
|
||||||
contentExcerpt: String
|
contentExcerpt: String
|
||||||
image: ImageInput,
|
image: ImageInput
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
@ -241,15 +251,19 @@ type Mutation {
|
|||||||
DeletePost(id: ID!): Post
|
DeletePost(id: ID!): Post
|
||||||
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||||
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||||
|
|
||||||
pinPost(id: ID!): Post
|
pinPost(id: ID!): Post
|
||||||
unpinPost(id: ID!): Post
|
unpinPost(id: ID!): Post
|
||||||
|
pinGroupPost(id: ID!): Post
|
||||||
|
unpinGroupPost(id: ID!): Post
|
||||||
|
|
||||||
markTeaserAsViewed(id: ID!): Post
|
markTeaserAsViewed(id: ID!): Post
|
||||||
pushPost(id: ID!): Post!
|
pushPost(id: ID!): Post!
|
||||||
unpushPost(id: ID!): Post!
|
unpushPost(id: ID!): Post!
|
||||||
|
|
||||||
# Shout the given Type and ID
|
"Shout the given Type and ID"
|
||||||
shout(id: ID!, type: ShoutTypeEnum!): Boolean!
|
shout(id: ID!, type: ShoutTypeEnum!): Boolean!
|
||||||
# Unshout the given Type and ID
|
"Unshout the given Type and ID"
|
||||||
unshout(id: ID!, type: ShoutTypeEnum!): Boolean!
|
unshout(id: ID!, type: ShoutTypeEnum!): Boolean!
|
||||||
|
|
||||||
toggleObservePost(id: ID!, value: Boolean!): Post!
|
toggleObservePost(id: ID!, value: Boolean!): Post!
|
||||||
|
|||||||
@ -17,7 +17,13 @@ enum ReportRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
|
reports(
|
||||||
|
orderBy: ReportOrdering
|
||||||
|
first: Int
|
||||||
|
offset: Int
|
||||||
|
reviewed: Boolean
|
||||||
|
closed: Boolean
|
||||||
|
): [Report]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReportOrdering {
|
enum ReportOrdering {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
# }
|
# }
|
||||||
|
|
||||||
# TODO change this to last message date
|
# TODO change this to last message date
|
||||||
|
|
||||||
enum _RoomOrdering {
|
enum _RoomOrdering {
|
||||||
lastMessageAt_desc
|
lastMessageAt_desc
|
||||||
createdAt_desc
|
createdAt_desc
|
||||||
@ -19,41 +20,48 @@ type Room {
|
|||||||
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
||||||
|
|
||||||
roomId: String! @cypher(statement: "RETURN this.id")
|
roomId: String! @cypher(statement: "RETURN this.id")
|
||||||
roomName: String! @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name")
|
roomName: String!
|
||||||
avatar: String @cypher(statement: """
|
@cypher(
|
||||||
MATCH (this)<-[:CHATS_IN]-(user:User)
|
statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name"
|
||||||
WHERE NOT user.id = $cypherParams.currentUserId
|
)
|
||||||
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image)
|
avatar: String
|
||||||
RETURN image.url
|
@cypher(
|
||||||
""")
|
statement: """
|
||||||
|
MATCH (this)<-[:CHATS_IN]-(user:User)
|
||||||
|
WHERE NOT user.id = $cypherParams.currentUserId
|
||||||
|
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image)
|
||||||
|
RETURN image.url
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
lastMessageAt: String
|
lastMessageAt: String
|
||||||
|
|
||||||
lastMessage: Message @cypher(statement: """
|
lastMessage: Message
|
||||||
MATCH (this)<-[:INSIDE]-(message:Message)
|
@cypher(
|
||||||
WITH message ORDER BY message.indexId DESC LIMIT 1
|
statement: """
|
||||||
RETURN message
|
MATCH (this)<-[:INSIDE]-(message:Message)
|
||||||
""")
|
WITH message ORDER BY message.indexId DESC LIMIT 1
|
||||||
|
RETURN message
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
unreadCount: Int @cypher(statement: """
|
unreadCount: Int
|
||||||
MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User)
|
@cypher(
|
||||||
WHERE NOT user.id = $cypherParams.currentUserId
|
statement: """
|
||||||
AND NOT message.seen
|
MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User)
|
||||||
RETURN count(message)
|
WHERE NOT user.id = $cypherParams.currentUserId
|
||||||
""")
|
AND NOT message.seen
|
||||||
|
RETURN count(message)
|
||||||
|
"""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateRoom(
|
CreateRoom(userId: ID!): Room
|
||||||
userId: ID!
|
|
||||||
): Room
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Room(
|
Room(id: ID, orderBy: [_RoomOrdering]): [Room]
|
||||||
id: ID
|
|
||||||
orderBy: [_RoomOrdering]
|
|
||||||
): [Room]
|
|
||||||
UnreadRooms: Int
|
UnreadRooms: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,4 +25,3 @@ type Statistics {
|
|||||||
usersVerified: Int!
|
usersVerified: Int!
|
||||||
reports: Int!
|
reports: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,8 @@ type Tag {
|
|||||||
id: ID!
|
id: ID!
|
||||||
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
|
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
|
||||||
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
|
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
|
||||||
taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
|
taggedCountUnique: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
}
|
}
|
||||||
@ -34,11 +35,5 @@ enum _TagOrdering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Tag(
|
Tag(id: ID, first: Int, offset: Int, orderBy: [_TagOrdering], filter: _TagFilter): [Tag]
|
||||||
id: ID
|
|
||||||
first: Int
|
|
||||||
offset: Int
|
|
||||||
orderBy: [_TagOrdering]
|
|
||||||
filter: _TagFilter
|
|
||||||
): [Tag]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,8 @@ type User {
|
|||||||
id: ID!
|
id: ID!
|
||||||
actorId: String
|
actorId: String
|
||||||
name: String
|
name: String
|
||||||
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
|
email: String!
|
||||||
|
@cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
|
||||||
slug: String!
|
slug: String!
|
||||||
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
@ -64,65 +65,78 @@ type User {
|
|||||||
emailNotificationSettings: [EmailNotificationSettings]! @neo4j_ignore
|
emailNotificationSettings: [EmailNotificationSettings]! @neo4j_ignore
|
||||||
locale: String
|
locale: String
|
||||||
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
||||||
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
friendsCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
|
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
|
||||||
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
|
followingCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
followedByCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
# Is the currently logged in user following that user?
|
"Is the currently logged in user following that user?"
|
||||||
followedByCurrentUser: Boolean! @cypher(
|
followedByCurrentUser: Boolean!
|
||||||
statement: """
|
@cypher(
|
||||||
MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId})
|
statement: "MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
|
||||||
RETURN COUNT(u) >= 1
|
)
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
isBlocked: Boolean! @cypher(
|
isBlocked: Boolean!
|
||||||
statement: """
|
@cypher(
|
||||||
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
statement: """
|
||||||
RETURN COUNT(user) >= 1
|
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
||||||
"""
|
RETURN COUNT(user) >= 1
|
||||||
)
|
"""
|
||||||
blocked: Boolean! @cypher(
|
)
|
||||||
statement: """
|
blocked: Boolean!
|
||||||
MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
@cypher(
|
||||||
RETURN COUNT(user) >= 1
|
statement: """
|
||||||
"""
|
MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
||||||
)
|
RETURN COUNT(user) >= 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
isMuted: Boolean! @cypher(
|
isMuted: Boolean!
|
||||||
statement: """
|
@cypher(
|
||||||
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
|
statement: """
|
||||||
RETURN COUNT(user) >= 1
|
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
|
||||||
"""
|
RETURN COUNT(user) >= 1
|
||||||
)
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# contributions: [WrittenPost]!
|
# contributions: [WrittenPost]!
|
||||||
# contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
# contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
||||||
# @cypher(
|
# @cypher(
|
||||||
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
|
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
|
||||||
# )
|
# )
|
||||||
|
|
||||||
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
|
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
|
||||||
contributionsCount: Int! @cypher(
|
contributionsCount: Int!
|
||||||
statement: """
|
@cypher(
|
||||||
MATCH (this)-[:WROTE]->(r:Post)
|
statement: """
|
||||||
WHERE NOT r.deleted = true AND NOT r.disabled = true
|
MATCH (this)-[:WROTE]->(r:Post)
|
||||||
RETURN COUNT(r)
|
WHERE NOT r.deleted = true AND NOT r.disabled = true
|
||||||
"""
|
RETURN COUNT(r)
|
||||||
)
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
||||||
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
|
commentedCount: Int!
|
||||||
|
@cypher(
|
||||||
|
statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))"
|
||||||
|
)
|
||||||
|
|
||||||
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
||||||
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
shoutedCount: Int!
|
||||||
|
@cypher(
|
||||||
|
statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
|
||||||
|
)
|
||||||
|
|
||||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
# Badges
|
# Badges
|
||||||
|
|
||||||
badgeVerification: Badge! @neo4j_ignore
|
badgeVerification: Badge! @neo4j_ignore
|
||||||
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||||
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||||
@ -133,22 +147,14 @@ type User {
|
|||||||
"personal inviteCodes the user has generated"
|
"personal inviteCodes the user has generated"
|
||||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||||
# inviteCodes: [InviteCode]! @relation(name: "GENERATED", direction: "OUT")
|
# inviteCodes: [InviteCode]! @relation(name: "GENERATED", direction: "OUT")
|
||||||
|
|
||||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||||
|
|
||||||
emotions: [EMOTED]
|
emotions: [EMOTED]
|
||||||
|
|
||||||
activeCategories: [String] @cypher(
|
activeCategories: [String] @neo4j_ignore
|
||||||
statement: """
|
|
||||||
MATCH (category:Category)
|
|
||||||
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
|
|
||||||
RETURN collect(category.id)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
myRoleInGroup: GroupMemberRole
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input _UserFilter {
|
input _UserFilter {
|
||||||
AND: [_UserFilter!]
|
AND: [_UserFilter!]
|
||||||
OR: [_UserFilter!]
|
OR: [_UserFilter!]
|
||||||
@ -215,7 +221,7 @@ enum Deletable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
UpdateUser (
|
UpdateUser(
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
email: String
|
email: String
|
||||||
@ -252,7 +258,7 @@ type Mutation {
|
|||||||
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
|
||||||
changePassword(oldPassword: String!, newPassword: String!): String!
|
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||||
|
|
||||||
# Get a JWT Token for the given Email and password
|
"Get a JWT Token for the given Email and password"
|
||||||
login(email: String!, password: String!): String!
|
login(email: String!, password: String!): String!
|
||||||
|
|
||||||
setTrophyBadgeSelected(slot: Int!, badgeId: ID): User
|
setTrophyBadgeSelected(slot: Int!, badgeId: ID): User
|
||||||
|
|||||||
@ -4,7 +4,5 @@ type UserData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
userData(
|
userData(id: ID): UserData
|
||||||
id: ID
|
|
||||||
): UserData
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// eslint-disable-next-line import/no-cycle
|
// import { addMiddleware } from '@middleware/index'
|
||||||
import { MiddlewareOrder } from '@middleware/index'
|
|
||||||
|
|
||||||
export default (): MiddlewareOrder[] => {
|
export default () => {
|
||||||
return []
|
// addMiddleware({ name: 'myMW', middleware: myMW, position: { } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
|
import { GraphQLResolveInfo } from 'graphql'
|
||||||
|
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
type Resolver = (
|
type Resolver = (
|
||||||
root: unknown,
|
root: unknown,
|
||||||
args: unknown,
|
args: unknown,
|
||||||
context: Context,
|
context: Context,
|
||||||
resolveInfo: unknown,
|
resolveInfo: GraphQLResolveInfo,
|
||||||
) => Promise<unknown>
|
) => Promise<unknown>
|
||||||
const checkCategoriesActive = (
|
const checkCategoriesActive = async (
|
||||||
resolve: Resolver,
|
resolve: Resolver,
|
||||||
root: unknown,
|
root: unknown,
|
||||||
args: unknown,
|
args: unknown,
|
||||||
context: Context,
|
context: Context,
|
||||||
resolveInfo: unknown,
|
resolveInfo: GraphQLResolveInfo,
|
||||||
) => {
|
): Promise<unknown> => {
|
||||||
if (context.config.CATEGORIES_ACTIVE) {
|
if (context.config.CATEGORIES_ACTIVE) {
|
||||||
return resolve(root, args, context, resolveInfo)
|
return resolve(root, args, context, resolveInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
223
backend/src/middleware/index.spec.ts
Normal file
223
backend/src/middleware/index.spec.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
/* eslint-disable n/global-require */
|
||||||
|
|
||||||
|
// Unit tests for addMiddleware – testing append, prepend, before, after, and error cases.
|
||||||
|
// Each test uses jest.isolateModules + jest.doMock to get a fresh ocelotMiddlewares array.
|
||||||
|
|
||||||
|
interface MiddlewareModule {
|
||||||
|
addMiddleware: (mw: { name: string; middleware: unknown; position: unknown }) => void
|
||||||
|
default: (schema: unknown) => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockOptions {
|
||||||
|
extraMocks?: Record<string, unknown>
|
||||||
|
disabledMiddlewares?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const middlewareModules = [
|
||||||
|
'./categories',
|
||||||
|
'./chatMiddleware',
|
||||||
|
'./excerptMiddleware',
|
||||||
|
'./hashtags/hashtagsMiddleware',
|
||||||
|
'./includedFieldsMiddleware',
|
||||||
|
'./languages/languages',
|
||||||
|
'./login/loginMiddleware',
|
||||||
|
'./notifications/notificationsMiddleware',
|
||||||
|
'./orderByMiddleware',
|
||||||
|
'./permissionsMiddleware',
|
||||||
|
'./sentryMiddleware',
|
||||||
|
'./sluggifyMiddleware',
|
||||||
|
'./softDelete/softDeleteMiddleware',
|
||||||
|
'./userInteractions',
|
||||||
|
'./validation/validationMiddleware',
|
||||||
|
'./xssMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
const setupMocks = ({ extraMocks, disabledMiddlewares = [] }: MockOptions = {}) => {
|
||||||
|
jest.doMock('./branding/brandingMiddlewares', () => jest.fn())
|
||||||
|
jest.doMock('@config/index', () => ({ DISABLED_MIDDLEWARES: disabledMiddlewares }))
|
||||||
|
|
||||||
|
// Mock all middlewares and allow to override its mock
|
||||||
|
for (const mod of middlewareModules) {
|
||||||
|
// eslint-disable-next-line security/detect-object-injection
|
||||||
|
jest.doMock(mod, () => extraMocks?.[mod] ?? {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadModule = (
|
||||||
|
options?: MockOptions,
|
||||||
|
): { mod: MiddlewareModule; getCapturedMiddlewares: () => unknown[] } => {
|
||||||
|
let capturedArgs: unknown[] = []
|
||||||
|
jest.doMock('graphql-middleware', () => ({
|
||||||
|
applyMiddleware: (_schema: unknown, ...middlewares: unknown[]) => {
|
||||||
|
capturedArgs = middlewares
|
||||||
|
return _schema
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
setupMocks(options)
|
||||||
|
// eslint-disable-next-line n/no-missing-require
|
||||||
|
const mod = require('./index') as MiddlewareModule
|
||||||
|
return {
|
||||||
|
mod,
|
||||||
|
getCapturedMiddlewares: () => {
|
||||||
|
mod.default({})
|
||||||
|
return capturedArgs
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('default', () => {
|
||||||
|
it('registers the 16 default middlewares', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { getCapturedMiddlewares } = loadModule()
|
||||||
|
expect(getCapturedMiddlewares()).toHaveLength(16)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls brandingMiddlewares', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { mod } = loadModule()
|
||||||
|
// eslint-disable-next-line n/no-missing-require
|
||||||
|
const brandingMiddlewares = require('./branding/brandingMiddlewares') as jest.Mock
|
||||||
|
mod.default({})
|
||||||
|
expect(brandingMiddlewares).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters out disabled middlewares', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const sentryMarker = { __test: 'sentry' }
|
||||||
|
const xssMarker = { __test: 'xss' }
|
||||||
|
const { getCapturedMiddlewares } = loadModule({
|
||||||
|
extraMocks: {
|
||||||
|
'./sentryMiddleware': sentryMarker,
|
||||||
|
'./xssMiddleware': xssMarker,
|
||||||
|
},
|
||||||
|
disabledMiddlewares: ['sentry', 'xss'],
|
||||||
|
})
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
|
||||||
|
const middlewares = getCapturedMiddlewares()
|
||||||
|
expect(middlewares).toHaveLength(14)
|
||||||
|
expect(middlewares).not.toContain(sentryMarker)
|
||||||
|
expect(middlewares).not.toContain(xssMarker)
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('Warning: Disabled "sentry, xss" middleware.')
|
||||||
|
consoleSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addMiddleware', () => {
|
||||||
|
describe('append', () => {
|
||||||
|
it('adds middleware at the end', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { mod, getCapturedMiddlewares } = loadModule()
|
||||||
|
const m = { __test: 'appended' }
|
||||||
|
mod.addMiddleware({ name: 'test-append', middleware: m, position: 'append' })
|
||||||
|
const middlewares = getCapturedMiddlewares()
|
||||||
|
expect(middlewares).toHaveLength(17)
|
||||||
|
expect(middlewares[16]).toBe(m)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prepend', () => {
|
||||||
|
it('adds middleware at the beginning', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { mod, getCapturedMiddlewares } = loadModule()
|
||||||
|
const m = { __test: 'prepended' }
|
||||||
|
mod.addMiddleware({ name: 'test-prepend', middleware: m, position: 'prepend' })
|
||||||
|
const middlewares = getCapturedMiddlewares()
|
||||||
|
expect(middlewares).toHaveLength(17)
|
||||||
|
expect(middlewares[0]).toBe(m)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('before', () => {
|
||||||
|
it('inserts middleware directly before the named anchor', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const sentryMarker = { __test: 'sentry' }
|
||||||
|
const permissionsMarker = { __test: 'permissions' }
|
||||||
|
const { mod, getCapturedMiddlewares } = loadModule({
|
||||||
|
extraMocks: {
|
||||||
|
'./sentryMiddleware': sentryMarker,
|
||||||
|
'./permissionsMiddleware': permissionsMarker,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const m = { __test: 'before-permissions' }
|
||||||
|
mod.addMiddleware({
|
||||||
|
name: 'test-before-permissions',
|
||||||
|
middleware: m,
|
||||||
|
position: { before: 'permissions' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const middlewares = getCapturedMiddlewares()
|
||||||
|
const idxSentry = middlewares.indexOf(sentryMarker)
|
||||||
|
const idxNew = middlewares.indexOf(m)
|
||||||
|
const idxPermissions = middlewares.indexOf(permissionsMarker)
|
||||||
|
|
||||||
|
expect(idxSentry).toBeLessThan(idxNew)
|
||||||
|
expect(idxNew).toBe(idxPermissions - 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after', () => {
|
||||||
|
it('inserts middleware directly after the named anchor', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const sentryMarker = { __test: 'sentry' }
|
||||||
|
const permissionsMarker = { __test: 'permissions' }
|
||||||
|
const { mod, getCapturedMiddlewares } = loadModule({
|
||||||
|
extraMocks: {
|
||||||
|
'./sentryMiddleware': sentryMarker,
|
||||||
|
'./permissionsMiddleware': permissionsMarker,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const m = { __test: 'after-sentry' }
|
||||||
|
mod.addMiddleware({
|
||||||
|
name: 'test-after-sentry',
|
||||||
|
middleware: m,
|
||||||
|
position: { after: 'sentry' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const middlewares = getCapturedMiddlewares()
|
||||||
|
const idxSentry = middlewares.indexOf(sentryMarker)
|
||||||
|
const idxNew = middlewares.indexOf(m)
|
||||||
|
const idxPermissions = middlewares.indexOf(permissionsMarker)
|
||||||
|
|
||||||
|
expect(idxNew).toBe(idxSentry + 1)
|
||||||
|
expect(idxNew).toBeLessThan(idxPermissions)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unknown anchor', () => {
|
||||||
|
it('throws when "before" anchor does not exist', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { mod } = loadModule()
|
||||||
|
expect(() =>
|
||||||
|
mod.addMiddleware({
|
||||||
|
name: 'failure',
|
||||||
|
middleware: {},
|
||||||
|
position: { before: 'nonexistent' },
|
||||||
|
}),
|
||||||
|
).toThrow('Could not find middleware "nonexistent" to append the middleware "failure"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when "after" anchor does not exist', () => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
const { mod } = loadModule()
|
||||||
|
expect(() =>
|
||||||
|
mod.addMiddleware({
|
||||||
|
name: 'failure',
|
||||||
|
middleware: {},
|
||||||
|
position: { after: 'nonexistent' },
|
||||||
|
}),
|
||||||
|
).toThrow('Could not find middleware "nonexistent" to append the middleware "failure"')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { applyMiddleware, IMiddleware } from 'graphql-middleware'
|
import { applyMiddleware, IMiddleware, IMiddlewareGenerator } from 'graphql-middleware'
|
||||||
|
|
||||||
import CONFIG from '@config/index'
|
import CONFIG from '@config/index'
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
|
||||||
import brandingMiddlewares from './branding/brandingMiddlewares'
|
import brandingMiddlewares from './branding/brandingMiddlewares'
|
||||||
import categories from './categories'
|
import categories from './categories'
|
||||||
import chatMiddleware from './chatMiddleware'
|
import chatMiddleware from './chatMiddleware'
|
||||||
@ -25,41 +24,79 @@ import validation from './validation/validationMiddleware'
|
|||||||
import xss from './xssMiddleware'
|
import xss from './xssMiddleware'
|
||||||
|
|
||||||
export interface MiddlewareOrder {
|
export interface MiddlewareOrder {
|
||||||
order: number
|
position: 'prepend' | 'append' | { before: string } | { after: string }
|
||||||
name: string
|
name: string
|
||||||
middleware: IMiddleware
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
middleware: IMiddleware | IMiddlewareGenerator<any, any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ocelotMiddlewares: MiddlewareOrder[] = [
|
const ocelotMiddlewares: MiddlewareOrder[] = []
|
||||||
{ order: -200, name: 'sentry', middleware: sentry },
|
|
||||||
{ order: -190, name: 'permissions', middleware: permissions },
|
export const addMiddleware = (middleware: MiddlewareOrder) => {
|
||||||
{ order: -180, name: 'xss', middleware: xss },
|
switch (middleware.position) {
|
||||||
{ order: -170, name: 'validation', middleware: validation },
|
case 'append':
|
||||||
{ order: -160, name: 'userInteractions', middleware: userInteractions },
|
ocelotMiddlewares.push(middleware)
|
||||||
{ order: -150, name: 'sluggify', middleware: sluggify },
|
break
|
||||||
{ order: -140, name: 'languages', middleware: languages },
|
case 'prepend':
|
||||||
{ order: -130, name: 'excerpt', middleware: excerpt },
|
ocelotMiddlewares.unshift(middleware)
|
||||||
{ order: -120, name: 'login', middleware: login },
|
break
|
||||||
{ order: -110, name: 'notifications', middleware: notifications },
|
default: {
|
||||||
{ order: -100, name: 'hashtags', middleware: hashtags },
|
const anchor =
|
||||||
{ order: -90, name: 'softDelete', middleware: softDelete },
|
'before' in middleware.position ? middleware.position.before : middleware.position.after
|
||||||
{ order: -80, name: 'includedFields', middleware: includedFields },
|
const appendMiddlewareAt = ocelotMiddlewares.findIndex((m) => m.name === anchor)
|
||||||
{ order: -70, name: 'orderBy', middleware: orderBy },
|
if (appendMiddlewareAt === -1) {
|
||||||
{ order: -60, name: 'chatMiddleware', middleware: chatMiddleware },
|
throw new Error(
|
||||||
{ order: -50, name: 'categories', middleware: categories },
|
`Could not find middleware "${anchor}" to append the middleware "${middleware.name}"`,
|
||||||
]
|
)
|
||||||
|
}
|
||||||
|
ocelotMiddlewares.splice(
|
||||||
|
appendMiddlewareAt + ('before' in middleware.position ? 0 : 1),
|
||||||
|
0,
|
||||||
|
middleware,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMiddleware({ name: 'sentry', middleware: sentry, position: 'append' })
|
||||||
|
addMiddleware({ name: 'permissions', middleware: permissions, position: { after: 'sentry' } })
|
||||||
|
addMiddleware({ name: 'xss', middleware: xss, position: { after: 'permissions' } })
|
||||||
|
addMiddleware({ name: 'validation', middleware: validation, position: { after: 'xss' } })
|
||||||
|
addMiddleware({
|
||||||
|
name: 'userInteractions',
|
||||||
|
middleware: userInteractions,
|
||||||
|
position: { after: 'validation' },
|
||||||
|
})
|
||||||
|
addMiddleware({ name: 'sluggify', middleware: sluggify, position: { after: 'userInteractions' } })
|
||||||
|
addMiddleware({ name: 'languages', middleware: languages, position: { after: 'sluggify' } })
|
||||||
|
addMiddleware({ name: 'excerpt', middleware: excerpt, position: { after: 'languages' } })
|
||||||
|
addMiddleware({ name: 'login', middleware: login, position: { after: 'excerpt' } })
|
||||||
|
addMiddleware({ name: 'notifications', middleware: notifications, position: { after: 'login' } })
|
||||||
|
addMiddleware({ name: 'hashtags', middleware: hashtags, position: { after: 'notifications' } })
|
||||||
|
addMiddleware({ name: 'softDelete', middleware: softDelete, position: { after: 'hashtags' } })
|
||||||
|
addMiddleware({
|
||||||
|
name: 'includedFields',
|
||||||
|
middleware: includedFields,
|
||||||
|
position: { after: 'softDelete' },
|
||||||
|
})
|
||||||
|
addMiddleware({ name: 'orderBy', middleware: orderBy, position: { after: 'includedFields' } })
|
||||||
|
addMiddleware({
|
||||||
|
name: 'chatMiddleware',
|
||||||
|
middleware: chatMiddleware,
|
||||||
|
position: { after: 'orderBy' },
|
||||||
|
})
|
||||||
|
addMiddleware({ name: 'categories', middleware: categories, position: { after: 'chatMiddleware' } })
|
||||||
|
|
||||||
export default (schema) => {
|
export default (schema) => {
|
||||||
const middlewares = ocelotMiddlewares
|
// execute branding middleware function
|
||||||
.concat(brandingMiddlewares())
|
brandingMiddlewares()
|
||||||
.sort((a, b) => a.order - b.order)
|
|
||||||
|
|
||||||
const filteredMiddlewares = middlewares.filter(
|
const filteredMiddlewares = ocelotMiddlewares.filter(
|
||||||
(middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name),
|
(middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Warn if we filtered
|
// Warn if we filtered
|
||||||
if (middlewares.length < filteredMiddlewares.length) {
|
if (ocelotMiddlewares.length !== filteredMiddlewares.length) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`)
|
console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -387,7 +387,7 @@ const isAllowedToGenerateGroupInviteCode = rule({
|
|||||||
return !!(
|
return !!(
|
||||||
await context.database.query({
|
await context.database.query({
|
||||||
query: `
|
query: `
|
||||||
MATCH (user:User{id: user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
MATCH (user:User{id: $user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
||||||
WHERE (group.type IN ['closed','hidden'] AND membership.role IN ['admin', 'owner'])
|
WHERE (group.type IN ['closed','hidden'] AND membership.role IN ['admin', 'owner'])
|
||||||
OR (NOT group.type IN ['closed','hidden'] AND NOT membership.role = 'pending')
|
OR (NOT group.type IN ['closed','hidden'] AND NOT membership.role = 'pending')
|
||||||
RETURN count(group) as count
|
RETURN count(group) as count
|
||||||
@ -397,6 +397,26 @@ const isAllowedToGenerateGroupInviteCode = rule({
|
|||||||
).records[0].get('count')
|
).records[0].get('count')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isAllowedToPinGroupPost = rule({
|
||||||
|
cache: 'no_cache',
|
||||||
|
})(async (_parent, args, context: Context) => {
|
||||||
|
if (!context.user) return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
await context.database.query({
|
||||||
|
query: `
|
||||||
|
MATCH (post:Post{id: $args.id})-[:IN]->(group:Group)
|
||||||
|
MATCH (user:User{id: $user.id})-[membership:MEMBER_OF]->(group)
|
||||||
|
WHERE (membership.role IN ['admin', 'owner'])
|
||||||
|
RETURN toString(count(group)) as count
|
||||||
|
`,
|
||||||
|
variables: { user: context.user, args },
|
||||||
|
})
|
||||||
|
).records[0].get('count') === '1'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
export default shield(
|
export default shield(
|
||||||
{
|
{
|
||||||
@ -485,6 +505,8 @@ export default shield(
|
|||||||
VerifyEmailAddress: isAuthenticated,
|
VerifyEmailAddress: isAuthenticated,
|
||||||
pinPost: isAdmin,
|
pinPost: isAdmin,
|
||||||
unpinPost: isAdmin,
|
unpinPost: isAdmin,
|
||||||
|
pinGroupPost: isAllowedToPinGroupPost,
|
||||||
|
unpinGroupPost: isAllowedToPinGroupPost,
|
||||||
pushPost: isAdmin,
|
pushPost: isAdmin,
|
||||||
unpushPost: isAdmin,
|
unpushPost: isAdmin,
|
||||||
UpdateDonations: isAdmin,
|
UpdateDonations: isAdmin,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ jest.mock('@aws-sdk/lib-storage', () => {
|
|||||||
|
|
||||||
const uploadMock = Upload as unknown as jest.Mock
|
const uploadMock = Upload as unknown as jest.Mock
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const createReadStream: FileUpload['createReadStream'] = (() => ({
|
const createReadStream: FileUpload['createReadStream'] = (() => ({
|
||||||
pipe: () => ({
|
pipe: () => ({
|
||||||
on: (_: unknown, callback: () => void) => callback(), // eslint-disable-line promise/prefer-await-to-callbacks
|
on: (_: unknown, callback: () => void) => callback(), // eslint-disable-line promise/prefer-await-to-callbacks
|
||||||
@ -32,6 +33,7 @@ const createReadStream: FileUpload['createReadStream'] = (() => ({
|
|||||||
const input = {
|
const input = {
|
||||||
uniqueFilename: 'unique-filename.jpg',
|
uniqueFilename: 'unique-filename.jpg',
|
||||||
mimetype: 'image/jpeg',
|
mimetype: 'image/jpeg',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
createReadStream,
|
createReadStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,9 @@ export const s3Service = (config: S3Config, prefix: string) => {
|
|||||||
Bucket,
|
Bucket,
|
||||||
Key: s3Location,
|
Key: s3Location,
|
||||||
ACL: ObjectCannedACL.public_read,
|
ACL: ObjectCannedACL.public_read,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
ContentType: mimetype,
|
ContentType: mimetype,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||||
Body: createReadStream(),
|
Body: createReadStream(),
|
||||||
}
|
}
|
||||||
const command = new Upload({ client: s3, params })
|
const command = new Upload({ client: s3, params })
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export const TEST_CONFIG = {
|
|||||||
INVITE_CODES_GROUP_PER_USER: 7,
|
INVITE_CODES_GROUP_PER_USER: 7,
|
||||||
CATEGORIES_ACTIVE: false,
|
CATEGORIES_ACTIVE: false,
|
||||||
MAX_PINNED_POSTS: 1,
|
MAX_PINNED_POSTS: 1,
|
||||||
|
MAX_GROUP_PINNED_POSTS: 1,
|
||||||
|
|
||||||
LANGUAGE_DEFAULT: 'en',
|
LANGUAGE_DEFAULT: 'en',
|
||||||
LOG_LEVEL: 'DEBUG',
|
LOG_LEVEL: 'DEBUG',
|
||||||
|
|||||||
6973
backend/yarn.lock
6973
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,55 @@
|
|||||||
const dotenv = require('dotenv')
|
const dotenv = require('dotenv')
|
||||||
const { defineConfig } = require('cypress');
|
const { defineConfig } = require('cypress');
|
||||||
const browserify = require('@cypress/browserify-preprocessor');
|
const webpackPreprocessor = require('@cypress/webpack-preprocessor');
|
||||||
const {
|
const {
|
||||||
addCucumberPreprocessorPlugin,
|
addCucumberPreprocessorPlugin,
|
||||||
} = require('@badeball/cypress-cucumber-preprocessor');
|
} = require('@badeball/cypress-cucumber-preprocessor');
|
||||||
const {
|
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
|
||||||
preprendTransformerToOptions,
|
const webpack = require('webpack');
|
||||||
} = require('@badeball/cypress-cucumber-preprocessor/browserify');
|
|
||||||
|
|
||||||
// Test persistent(between commands) store
|
// Test persistent(between commands) store
|
||||||
const testStore = {}
|
const testStore = {}
|
||||||
|
|
||||||
async function setupNodeEvents(on, config) {
|
async function setupNodeEvents(on, config) {
|
||||||
|
// This is required for the preprocessor to be able to generate JSON reports after each run, and more
|
||||||
await addCucumberPreprocessorPlugin(on, config);
|
await addCucumberPreprocessorPlugin(on, config);
|
||||||
|
|
||||||
on(
|
on(
|
||||||
'file:preprocessor',
|
'file:preprocessor',
|
||||||
browserify(preprendTransformerToOptions(config, browserify.defaultOptions)),
|
webpackPreprocessor({
|
||||||
|
webpackOptions: {
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'source-map',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.json'],
|
||||||
|
fallback: {
|
||||||
|
fs: false,
|
||||||
|
net: false,
|
||||||
|
tls: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.feature$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@badeball/cypress-cucumber-preprocessor/webpack',
|
||||||
|
options: config,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new NodePolyfillPlugin(),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
process: 'process/browser',
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
on('task', {
|
on('task', {
|
||||||
|
|||||||
@ -2,6 +2,6 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
|||||||
|
|
||||||
defineStep('I open the content menu of post {string}', (title) => {
|
defineStep('I open the content menu of post {string}', (title) => {
|
||||||
cy.contains('.post-teaser', title)
|
cy.contains('.post-teaser', title)
|
||||||
.find('.content-menu .base-button')
|
.find('[data-test="content-menu-button"]')
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
|||||||
|
|
||||||
defineStep('I click on "Report Post" from the content menu of the post', () => {
|
defineStep('I click on "Report Post" from the content menu of the post', () => {
|
||||||
cy.contains('.base-card', 'The Truth about the Holocaust')
|
cy.contains('.base-card', 'The Truth about the Holocaust')
|
||||||
.find('.content-menu .base-button')
|
.find('[data-test="content-menu-button"]')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.popover .ds-menu-item-link')
|
cy.get('.popover .ds-menu-item-link')
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
|
||||||
defineStep('I should see the {string} button', button => {
|
defineStep('I should see the {string} button', button => {
|
||||||
cy.get('.base-card .action-buttons .base-button')
|
cy.get('.base-card .action-buttons button')
|
||||||
.should('contain', button)
|
.should('contain', button)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
|
||||||
defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => {
|
defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => {
|
||||||
cy.get('.user-content-menu .base-button').click()
|
cy.get('.user-content-menu [data-test="content-menu-button"]').click()
|
||||||
cy.get('.popover .ds-menu-item-link')
|
cy.get('.popover .ds-menu-item-link')
|
||||||
.should(condition === 'should' ? 'contain' : 'not.contain', link)
|
.should(condition === 'should' ? 'contain' : 'not.contain', link)
|
||||||
})
|
})
|
||||||
|
|||||||
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