Compare commits

..

136 Commits

Author SHA1 Message Date
dependabot[bot]
55e8746833
build(deps-dev): bump vite-tsconfig-paths from 6.1.0 to 6.1.1 in /packages/ui in the vite group (#9225)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 00:25:34 +00:00
dependabot[bot]
0a6e47008e
build(deps-dev): bump @storybook/vue3-vite from 10.2.7 to 10.2.8 in /packages/ui (#9229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 23:59:51 +00:00
179bf6983c
chore(release): v3.14.1 (#9213) 2026-02-14 23:24:28 +00:00
36e9ad6f80
refactor(package/ui): eslint config it4c update (#9233) 2026-02-14 22:43:38 +00:00
dependabot[bot]
72714f58a6
build(deps): bump @aws-sdk/client-s3 from 3.985.0 to 3.990.0 in /backend (#9224)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 15:33:24 +01:00
dependabot[bot]
29277341b2
build(deps-dev): bump eslint-plugin-vuejs-accessibility from 2.4.1 to 2.5.0 in /packages/ui (#9226)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 13:58:12 +00:00
dependabot[bot]
1908332279
build(deps-dev): bump eslint-plugin-storybook from 10.2.7 to 10.2.8 in /packages/ui (#9228)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 13:34:33 +00:00
dependabot[bot]
231473644d
build(deps-dev): bump glob from 13.0.1 to 13.0.3 in /packages/ui (#9230)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 12:54:47 +00:00
dependabot[bot]
6024e63308
build(deps-dev): bump @types/node from 25.2.2 to 25.2.3 in /packages/ui (#9232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 13:29:45 +01:00
f4fe8553de
fix(package/ui): os-button class to ensure branding compatibility (#9211) 2026-02-14 00:02:43 +01:00
794b4dabfa
refactor(webapp): vue3 migration - button - icon + circle + loading (#9208) 2026-02-13 16:27:33 +00:00
91fac6f7c6
fix(workflow): fix workflow not to double build the webapp image when running unit test (#9210) 2026-02-13 16:04:17 +00:00
dependabot[bot]
93309bf4f3
build(deps-dev): bump the babel group with 3 updates (#9109)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 00:52:16 +01:00
dependabot[bot]
21036c5391
build(deps): bump email-templates from 12.0.3 to 13.0.1 in /backend (#9091)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 00:15:18 +01:00
dependabot[bot]
d38bd9de65
build(deps-dev): bump eslint-plugin-jest from 29.12.2 to 29.13.0 in /backend (#9186)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 22:06:06 +00:00
5d204e0254
fix(backend): fix categories filter (#9209) 2026-02-11 19:58:22 +00:00
dependabot[bot]
3cb643754d
build(deps-dev): bump eslint-plugin-jsonc from 2.21.0 to 2.21.1 in /backend (#9188)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 17:15:07 +01:00
dependabot[bot]
c993b2862b
build(deps-dev): bump dotenv from 17.2.3 to 17.2.4 (#9166)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 13:49:47 +01:00
dependabot[bot]
5ec508b33c
build(deps): bump actions/setup-node from 4 to 6 (#9184)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 12:57:21 +01:00
dependabot[bot]
b7c09eee7f
build(deps): bump actions/checkout from 4 to 6 (#9185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 12:01:24 +01:00
dependabot[bot]
5dfb000f45
build(deps): bump actions/upload-artifact from 4 to 6 (#9182)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 04:14:45 +00:00
761305e333
feat(webapp): correct version + commits (#9203) 2026-02-11 03:42:03 +00:00
604de30fa2
fix(workflow): rename ui compatibility test (#9207) 2026-02-11 02:51:27 +00:00
07cf1eacc9
refactor(workflow): cache packages (#9206) 2026-02-11 00:46:04 +00:00
9b6d2bbbba
refactor(workflow): all e2e are running in parallel (#9205) 2026-02-11 01:17:26 +01:00
0d617c46c6
fix(workflow): ensure ui workflows always run, but be skipped if not needed (#9204) 2026-02-10 23:09:37 +00:00
dependabot[bot]
c32ea0f43b
build(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /backend (#9187)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 22:36:17 +00:00
eaac170a60
fix(webapp): properly switch language on static pages (#9202) 2026-02-10 22:05:57 +00:00
7b3c907cf6
fix(webapp): make static pages available when logged out (#9201) 2026-02-10 21:32:12 +00:00
9b98dcae9a
refactor(webapp): vue3 migration - phase 3 - integration (#9180) 2026-02-10 20:56:32 +00:00
f2e77595b2
fix(backend): ensure a pinned post is accessible even tho the user was muted (#9200) 2026-02-10 18:55:33 +00:00
04effaa506
fix(backend): fix structure of unit test reports (#9199) 2026-02-10 17:13:07 +01:00
75e36abbc6
refactor(package/ui): extract rules to eslint config it4c & update package (#9198) 2026-02-10 14:44:51 +00:00
080923a0e4
refactor(workflow): remove auto-approve workflow (#9197) 2026-02-10 14:10:56 +00:00
901fc01ca6
refactor(workflow): add a new scope for PRs: package/ui (#9196) 2026-02-10 13:47:45 +00:00
dependabot[bot]
accb62d8ae
build(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /packages/ui (#9193)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 03:34:45 +00:00
dependabot[bot]
3da25b6519
build(deps-dev): bump vue from 3.5.27 to 3.5.28 in /packages/ui in the vue group (#9191)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 03:14:31 +00:00
c33ec0bd11
refactor(backend): reports query parameterization and resolver cleanup with test coverage (#9156) 2026-02-09 19:05:59 +00:00
7162f3bd4e
fix(workflow): allow code rabbit to approve PRs (#9195) 2026-02-09 18:02:25 +00:00
66d17db54b
feat(workflow): coderabbit (#9194) 2026-02-09 13:11:41 +00:00
1f8f902a28
chore(release): v3.14.0 (#9181) 2026-02-09 12:47:48 +00:00
5d1cabda46
refactor(webapp): vue3 migration - phase 2 - setup (#9161) 2026-02-09 11:53:12 +01:00
dependabot[bot]
f945a4bafc
build(deps): bump nginx from 1.29.4-alpine to 1.29.5-alpine in /webapp (#9162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 04:25:43 +00:00
dependabot[bot]
b39782b788
build(deps): bump node from 25.5.0-alpine to 25.6.0-alpine in /backend (#9163)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 03:34:59 +00:00
dependabot[bot]
99bf691ecb
build(deps): bump node from 25.5.0-alpine to 25.6.0-alpine in /webapp (#9164)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 04:14:15 +01:00
dependabot[bot]
9814c3e395
build(deps-dev): bump cypress from 15.9.0 to 15.10.0 in the cypress group (#9165)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 00:53:33 +00:00
dependabot[bot]
e0b3b7d375
build(deps-dev): bump webpack from 5.104.1 to 5.105.0 (#9167)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 00:14:48 +00:00
dependabot[bot]
5f5dced68e
build(deps): bump nodemailer from 7.0.13 to 8.0.0 in /backend (#9168)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 00:44:31 +01:00
dependabot[bot]
0190f52dfc
build(deps): bump minimatch from 10.1.1 to 10.1.2 in /backend (#9169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 22:59:33 +00:00
dependabot[bot]
5b3e99bf76
build(deps-dev): bump @types/node from 25.1.0 to 25.2.1 in /backend (#9171)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 22:34:27 +00:00
dependabot[bot]
b526e1ffb4
build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend (#9173)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 22:09:03 +00:00
8806abda6c
refactor(workflow): do not clean cache after run (#9155)
Co-authored-by: mahula <lenzmath@posteo.de>
2026-02-07 21:48:01 +00:00
dependabot[bot]
5294ab963a
build(deps-dev): bump eslint-plugin-jest from 29.12.1 to 29.12.2 in /backend (#9177)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 21:16:44 +00:00
dependabot[bot]
fac44d734e
build(deps): bump peter-evans/repository-dispatch from cf70392543065ca62813db6712a06df1c4f4ae9f to f49a8ac5751834a0666df77deb0289abbe2b3a78 (#9179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 21:54:34 +01:00
0e4e72429d
refactor(webapp): vue 2.7.16 (#9160) 2026-02-04 10:32:18 +00:00
a78c25a258
refactor(backend): test roles (#9157) 2026-02-03 16:00:37 +00:00
753a300c3f
refactor(backend): middleware before/after (#9128) 2026-02-03 14:20:19 +01:00
dependabot[bot]
b28cdada6d
build(deps): bump node from 25.4.0-alpine to 25.5.0-alpine in /webapp (#9147)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 19:36:31 +00:00
dependabot[bot]
8a04b09fd7
build(deps): bump node from 25.4.0-alpine to 25.5.0-alpine in /backend (#9148)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 19:10:07 +00:00
dependabot[bot]
b26a06f0ef
build(deps): bump actions/cache from 5.0.2 to 5.0.3 (#9149)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 18:50:29 +00:00
dependabot[bot]
e5231acd4f
build(deps): bump docker/login-action from 3.6.0 to 3.7.0 (#9150)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 18:24:21 +00:00
dependabot[bot]
8e8bab6f9d
build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend (#9151)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 18:04:36 +00:00
dependabot[bot]
71228260e5
build(deps-dev): bump @types/node from 25.0.10 to 25.1.0 in /backend (#9152)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 17:41:59 +00:00
d96cb32f11
refactor(backend): properly model group-membership (#9124) 2026-01-30 04:56:03 +01:00
bea7c275e8
fix(webapp): allow internal path for custom button (#9129) 2026-01-29 18:51:53 +01:00
07ff0a6b5e
feat(backend): db script disable notifications (#9131) 2026-01-28 22:14:35 +01:00
6fc3c03860 feat(backend): group pins (#9034)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2026-01-28 16:53:29 +01:00
524c4caf5e
refactor(backend): lint graphql (#8473) 2026-01-28 14:42:53 +01:00
8136ec1aba
fix(backend): fix bug in notifications settings for currentUser (#9130) 2026-01-28 00:36:51 +01:00
0ee476cfff
fix(backend): fix email url encoding (#9127) 2026-01-27 22:39:40 +00:00
Wolfgang Huß
b39d1c737b
refactor(other): consolidate Node.js versions and fix e2e workflow (#9126)
Co-authored-by: mahula <lenzmath@posteo.de>
2026-01-27 18:23:26 +01:00
dependabot[bot]
dcff378727
build(deps): bump cheerio from 1.1.2 to 1.2.0 in /backend (#9141)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-25 17:12:12 +00:00
dependabot[bot]
6c34da94f4
build(deps-dev): bump @types/node from 25.0.9 to 25.0.10 in /backend (#9142)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-25 15:26:12 +00:00
dependabot[bot]
f5f6ceb2c5
build(deps): bump @aws-sdk/client-s3 and @aws-sdk/lib-storage in /backend (#9144)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-25 15:04:44 +00:00
dependabot[bot]
e109ac29b7
build(deps): bump peter-evans/repository-dispatch from 09094272a794c6105029af051e3831908c649b6c to cf70392543065ca62813db6712a06df1c4f4ae9f (#9145)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-25 14:17:06 +00:00
dependabot[bot]
05aeb1c20d
build(deps): bump the metascraper group in /backend with 12 updates (#9136)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-25 14:47:47 +01:00
dependabot[bot]
003ec2bda0
build(deps-dev): bump sass-embedded from 1.97.2 to 1.97.3 (#9135)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 22:24:46 +00:00
dependabot[bot]
208a6dca01
build(deps): bump preview-email from 3.1.0 to 3.1.1 in /backend (#9138)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 20:30:49 +00:00
af497deb77
fix(webapp): allow running frontend tests locally (#9125) 2026-01-24 20:09:36 +00:00
dependabot[bot]
ba481547f1
build(deps): bump node from 25.3.0-alpine to 25.4.0-alpine in /webapp (#9133)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 19:41:50 +00:00
dependabot[bot]
9f4c105335
build(deps-dev): bump @cucumber/cucumber from 12.5.0 to 12.6.0 in the cypress group (#9134)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 18:38:52 +00:00
dependabot[bot]
8012d56dc8
build(deps): bump lodash from 4.17.21 to 4.17.23 in /backend (#9140)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 15:02:14 +00:00
dependabot[bot]
9d994a7554
build(deps-dev): bump prettier from 3.8.0 to 3.8.1 in /webapp (#9139)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 14:41:41 +00:00
dependabot[bot]
322d2aeb97
build(deps-dev): bump prettier from 3.8.0 to 3.8.1 in /backend (#9143)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 14:15:01 +00:00
dependabot[bot]
afee1033af
build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#9146)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 13:48:54 +00:00
dependabot[bot]
d358fdf6b4
build(deps): bump node from 25.3.0-alpine to 25.4.0-alpine in /backend (#9132)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 11:55:14 +00:00
150b318aab
feat(backend): admin creation command for production (#9057)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2026-01-19 18:36:20 +00:00
f0f9b7faec
fix(backend): fix permissions for GroupInviteCodes (#9121)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2026-01-19 18:14:17 +00:00
b22974031c
fix(backend): fix group-myRole field query (#9102)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2026-01-19 17:48:25 +00:00
mahula
0ca45dd06e
refactor(e2e): optimize step definitions loading with filepart pairing (#9122) 2026-01-19 15:00:40 +00:00
6a42d12fda
fix(webapp): fix cta-join-group, can crash when group is not defined (#9103) 2026-01-19 12:05:09 +00:00
b7604e9af5
fix(backend): fix active categories when inproperly configured (#9123)
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2026-01-19 11:29:26 +00:00
3d00ae4e25
fix(webapp): fix local webapp tests (#9104)
Co-authored-by: mahula <lenzmath@posteo.de>
2026-01-18 11:55:40 +00:00
dependabot[bot]
fa71b0e189
build(deps-dev): bump the cypress group across 1 directory with 3 updates (#9058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-18 12:29:03 +01:00
dependabot[bot]
855a049f90
build(deps): bump node from 25.2.1-alpine to 25.3.0-alpine in /webapp (#9105)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-18 00:12:19 +00:00
dependabot[bot]
679e4876bc
build(deps): bump actions/setup-node from 6.1.0 to 6.2.0 (#9107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 23:45:25 +00:00
dependabot[bot]
e84a81bd2f
build(deps): bump node from 25.2.1-alpine to 25.3.0-alpine in /backend (#9106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 23:25:20 +00:00
dependabot[bot]
34e547553e
build(deps): bump actions/cache from 5.0.1 to 5.0.2 (#9108)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-18 00:04:22 +01:00
dependabot[bot]
5aa298b3a2
build(deps-dev): bump eslint-plugin-n from 17.23.1 to 17.23.2 in /backend (#9110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 22:29:23 +00:00
dependabot[bot]
626372a741
build(deps-dev): bump @types/lodash from 4.17.21 to 4.17.23 in /backend (#9111)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 22:04:58 +00:00
dependabot[bot]
a0ac5157a1
build(deps): bump @aws-sdk/lib-storage from 3.958.0 to 3.967.0 in /backend (#9113)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 21:43:30 +00:00
dependabot[bot]
083d81be89
build(deps-dev): bump eslint-plugin-prettier from 5.5.4 to 5.5.5 in /backend (#9114)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 21:22:21 +00:00
dependabot[bot]
721fd75288
build(deps-dev): bump @eslint-community/eslint-plugin-eslint-comments from 4.5.0 to 4.6.0 in /backend (#9120)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 20:57:08 +00:00
dependabot[bot]
7e6d79f1dc
build(deps-dev): bump prettier from 3.7.4 to 3.8.0 in /webapp (#9117)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 20:35:57 +00:00
dependabot[bot]
bc7e750e83
build(deps): bump ioredis from 5.9.1 to 5.9.2 in /backend (#9119)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 20:07:42 +00:00
dependabot[bot]
fbe98aa2b4
build(deps): bump @aws-sdk/client-s3 from 3.967.0 to 3.971.0 in /backend (#9118)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 19:46:46 +00:00
dependabot[bot]
7ae516cf85
build(deps-dev): bump eslint-plugin-prettier from 5.5.4 to 5.5.5 in /webapp (#9115)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 19:22:19 +00:00
dependabot[bot]
02bf7f0ab8
build(deps-dev): bump prettier from 3.7.4 to 3.8.0 in /backend (#9116)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 19:54:52 +01:00
dependabot[bot]
cc8ab95eaf
build(deps-dev): bump @types/node from 25.0.7 to 25.0.9 in /backend (#9112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 17:59:37 +01:00
dependabot[bot]
017bfbc820
build(deps): bump the metascraper group in /backend with 12 updates (#9063)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 10:59:59 +00:00
dependabot[bot]
25eeb8d485
build(deps-dev): bump eslint-plugin-jest from 29.12.0 to 29.12.1 in /backend (#9090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 10:32:48 +01:00
dependabot[bot]
eca7f5096e
build(deps): bump ioredis from 5.8.2 to 5.9.1 in /backend (#9095)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 09:02:33 +00:00
dependabot[bot]
9f581f4773
build(deps): bump @aws-sdk/client-s3 from 3.958.0 to 3.966.0 in /backend (#9100)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 10:43:05 +00:00
dependabot[bot]
b767e02263
build(deps): bump @aws-sdk/lib-storage from 3.933.0 to 3.958.0 in /backend (#9093)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 11:20:48 +01:00
dependabot[bot]
b01d5e5a27
build(deps-dev): bump @types/node from 25.0.3 to 25.0.5 in /backend (#9096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 10:54:01 +01:00
dependabot[bot]
06a79225f3
build(deps): bump peter-evans/repository-dispatch from 46fabd2783425293d3f24bc1080da28d046e2dd3 to 09094272a794c6105029af051e3831908c649b6c (#9089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 09:34:38 +01:00
dependabot[bot]
eaa9b34d58
build(deps): bump vue-advanced-chat from 2.0.11 to 2.1.2 in /webapp (#9084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 15:54:02 +00:00
dependabot[bot]
c4fcd558e3
build(deps): bump nginx from 1.29.3-alpine to 1.29.4-alpine in /webapp (#9070)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 15:09:57 +00:00
dependabot[bot]
0fef81464f
build(deps): bump @aws-sdk/client-s3 from 3.933.0 to 3.958.0 in /backend (#9086)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 12:52:30 +00:00
dependabot[bot]
9c3d3e2fcd
build(deps-dev): bump @types/node from 24.10.1 to 25.0.3 in /backend (#9078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 10:40:44 +00:00
dependabot[bot]
cbb57622f7
build(deps-dev): bump eslint-plugin-jest from 29.1.0 to 29.11.0 in /backend (#9087)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 11:19:47 +01:00
dependabot[bot]
861275aeda
build(deps): bump express from 5.1.0 to 5.2.1 in /backend (#9065)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 12:08:44 +01:00
dependabot[bot]
c0c396653f
build(deps-dev): bump @types/lodash from 4.17.20 to 4.17.21 in /backend (#9051)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 10:42:27 +00:00
dependabot[bot]
5642e0db2c
build(deps-dev): bump ts-jest from 29.4.5 to 29.4.6 in /backend (#9069)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 10:20:32 +00:00
dependabot[bot]
49f7118468
build(deps): bump validator from 13.15.23 to 13.15.26 in /webapp (#9083)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 09:37:50 +00:00
dependabot[bot]
9a0c97e6ce
build(deps): bump validator from 13.15.23 to 13.15.26 in /backend (#9080)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 09:10:01 +00:00
dependabot[bot]
1dd7fc3d75
build(deps): bump nodemailer from 7.0.10 to 7.0.12 in /backend (#9088)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 08:48:33 +00:00
dependabot[bot]
0e2d90c634
build(deps): bump actions/upload-artifact from 5.0.0 to 6.0.0 (#9072)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 09:26:57 +01:00
dependabot[bot]
56338422a2
build(deps): bump peter-evans/repository-dispatch from d2c43ab06ec1cddd2c2a0aae659681b8465ce87a to 46fabd2783425293d3f24bc1080da28d046e2dd3 (#9060)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 15:30:08 +00:00
dependabot[bot]
817ac7226e
build(deps-dev): bump prettier from 3.6.2 to 3.7.4 in /webapp (#9059)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 14:05:00 +00:00
dependabot[bot]
eb81c0b7e4
build(deps): bump docker/metadata-action from 5.9.0 to 5.10.0 (#9049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 13:38:02 +00:00
dependabot[bot]
34aa894068
build(deps): bump actions/setup-node from 6.0.0 to 6.1.0 (#9061)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 14:17:19 +01:00
dependabot[bot]
d751e7090f
build(deps): bump gaurav-nelson/github-action-markdown-link-check from 1.0.16 to 1.0.17 (#8329)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 10:15:30 +00:00
dependabot[bot]
a9949e1147
build(deps): bump actions/cache from 4.3.0 to 5.0.1 (#9071)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 09:48:11 +00:00
dependabot[bot]
c78f8deee9
build(deps): bump actions/checkout from 5.0.0 to 6.0.1 (#9062)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 10:04:02 +00:00
dependabot[bot]
2cabe0f4d2
build(deps-dev): bump prettier from 3.6.2 to 3.7.4 in /backend (#9067)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 10:09:34 +01:00
391 changed files with 83071 additions and 13807 deletions

99
.coderabbit.yaml Normal file
View 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

View File

@ -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"

View File

@ -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'

View File

@ -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'

View File

@ -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 }}

View File

@ -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

View File

@ -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: |

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

2
.nvmrc
View File

@ -1 +1 @@
v24.2.0 v25.3.0

View File

@ -0,0 +1,3 @@
{
"packages/ui": "0.0.1"
}

View File

@ -1 +0,0 @@
nodejs 20.12.1

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,
},
},
], ],
} }

View File

@ -1 +1 @@
v24.2.0 v25.3.0

View File

@ -1 +0,0 @@
nodejs 24.2.0

View File

@ -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

View File

@ -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/) (&gt;= `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/):

View File

@ -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"

View File

@ -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 = {

View 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()
})()

View File

@ -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',

View File

@ -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,
}

View File

@ -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),
}, },
}) })

View File

@ -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
}
} }
} }
` `

View File

@ -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
}
} }
} }
` `

View File

@ -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
}
} }
} }
` `

View File

@ -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
}
} }
} }
` `

View File

@ -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
}
} }
} }
` `

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const availableRoles = gql`
query {
availableRoles
}
`

View 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
}
}
`

View File

@ -11,6 +11,7 @@ export const profilePagePosts = gql`
id id
title title
content content
groupPinned
} }
} }
` `

View File

@ -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

View 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
}
}
`

View File

@ -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 {

View File

@ -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,

View File

@ -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'

View File

@ -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' },
) )

View File

@ -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,

View File

@ -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
}) })
} }

View File

@ -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

View File

@ -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 })
} }

View File

@ -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',
},
}, },
]), ]),
}, },

View File

@ -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

View 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 }),
],
},
})
})
})
})

View File

@ -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

View File

@ -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)
})
})
}) })
}) })
}) })

View File

@ -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

View 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'])
})
})
})
})

View File

@ -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) => {

View File

@ -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:

View File

@ -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', () => {

View File

@ -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')
})
})
}) })
}) })

View File

@ -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'

View 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

View File

@ -2,4 +2,4 @@ enum EmailNotificationSettingsType {
post post
chat chat
group group
} }

View File

@ -4,4 +4,4 @@ enum Emotion {
happy happy
angry angry
funny funny
} }

View File

@ -1,4 +1,4 @@
enum ShoutTypeEnum { enum ShoutTypeEnum {
Post Post
Comment Comment
} }

View File

@ -2,4 +2,4 @@ enum Visibility {
public public
friends friends
private private
} }

View File

@ -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
} }

View File

@ -13,4 +13,4 @@ type Query {
type Mutation { type Mutation {
UpdateDonations(showDonations: Boolean, goal: Int, progress: Int): Donations UpdateDonations(showDonations: Boolean, goal: Int, progress: Int): Donations
} }

View File

@ -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
} }

View File

@ -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
}

View File

@ -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
} }

View File

@ -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,20 +83,16 @@ 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
# AvailableGroupTypes: [GroupType]! # AvailableGroupTypes: [GroupType]!
# AvailableGroupActionRadii: [GroupActionRadius]! # AvailableGroupActionRadii: [GroupActionRadius]!
@ -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,27 +133,14 @@ 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
} }

View File

@ -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
} }

View File

@ -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!
} }

View File

@ -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!

View File

@ -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 {

View File

@ -33,7 +33,7 @@ enum NotificationReason {
type Query { type Query {
notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED] notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED]
} }
type Mutation { type Mutation {
markAsRead(id: ID!): NOTIFIED markAsRead(id: ID!): NOTIFIED
markAllAsRead: [NOTIFIED] markAllAsRead: [NOTIFIED]

View File

@ -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"
@ -173,15 +182,14 @@ type Post {
@cypher( @cypher(
statement: "MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1" statement: "MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
) )
emotions: [EMOTED] emotions: [EMOTED]
emotionsCount: Int! emotionsCount: Int!
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
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!

View File

@ -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 {

View File

@ -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
} }

View File

@ -25,4 +25,3 @@ type Statistics {
usersVerified: Int! usersVerified: Int!
reports: Int! reports: Int!
} }

View File

@ -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]
} }

View File

@ -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(
statement: """
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
"""
)
isMuted: Boolean! @cypher(
statement: """
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!]
@ -203,7 +209,7 @@ type Query {
filter: _UserFilter filter: _UserFilter
): [User] ): [User]
availableRoles: [UserRole]! availableRoles: [UserRole]!
mutedUsers: [User] mutedUsers: [User]
blockedUsers: [User] blockedUsers: [User]
currentUser: User! currentUser: User!
@ -215,7 +221,7 @@ enum Deletable {
} }
type Mutation { type Mutation {
UpdateUser ( UpdateUser(
id: ID! id: ID!
name: String name: String
email: String email: String
@ -245,14 +251,14 @@ type Mutation {
switchUserRole(role: UserRole!, id: ID!): User switchUserRole(role: UserRole!, id: ID!): User
saveCategorySettings(activeCategories: [String]): Boolean saveCategorySettings(activeCategories: [String]): Boolean
updateOnlineStatus(status: OnlineStatus!): Boolean! updateOnlineStatus(status: OnlineStatus!): Boolean!
requestPasswordReset(email: String!, locale: String!): Boolean! requestPasswordReset(email: String!, locale: String!): Boolean!
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

View File

@ -4,7 +4,5 @@ type UserData {
} }
type Query { type Query {
userData( userData(id: ID): UserData
id: ID
): UserData
} }

View File

@ -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: { } })
} }

View File

@ -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)
} }

View 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"')
})
})
})
})

View File

@ -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.`)
} }

View File

@ -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,

View File

@ -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,
} }

View File

@ -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 })

View File

@ -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',

Some files were not shown because too many files have changed in this diff Show More