mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-03 16:15:36 +00:00
Compare commits
No commits in common. "master" and "b3.14.1-42" have entirely different histories.
master
...
b3.14.1-42
@ -33,7 +33,6 @@ reviews:
|
|||||||
- "!**/package-lock.json"
|
- "!**/package-lock.json"
|
||||||
- "!**/yarn.lock"
|
- "!**/yarn.lock"
|
||||||
- "!**/*.snap"
|
- "!**/*.snap"
|
||||||
- "!**/*.png"
|
|
||||||
- "!**/coverage/**"
|
- "!**/coverage/**"
|
||||||
- "!**/dist/**"
|
- "!**/dist/**"
|
||||||
- "!**/node_modules/**"
|
- "!**/node_modules/**"
|
||||||
|
|||||||
37
.github/dependabot.yml
vendored
37
.github/dependabot.yml
vendored
@ -127,43 +127,6 @@ updates:
|
|||||||
timezone: "Europe/Berlin"
|
timezone: "Europe/Berlin"
|
||||||
time: "03:00"
|
time: "03:00"
|
||||||
|
|
||||||
# maintenance
|
|
||||||
- package-ecosystem: docker
|
|
||||||
open-pull-requests-limit: 99
|
|
||||||
directory: "/maintenance"
|
|
||||||
rebase-strategy: "disabled"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: "saturday"
|
|
||||||
timezone: "Europe/Berlin"
|
|
||||||
time: "03:00"
|
|
||||||
- package-ecosystem: npm
|
|
||||||
open-pull-requests-limit: 99
|
|
||||||
directory: "/maintenance"
|
|
||||||
rebase-strategy: "disabled"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: "saturday"
|
|
||||||
timezone: "Europe/Berlin"
|
|
||||||
time: "03:00"
|
|
||||||
groups:
|
|
||||||
nuxt:
|
|
||||||
applies-to: version-updates
|
|
||||||
patterns:
|
|
||||||
- "nuxt*"
|
|
||||||
- "@nuxt*"
|
|
||||||
- "@nuxtjs*"
|
|
||||||
vitest:
|
|
||||||
applies-to: version-updates
|
|
||||||
patterns:
|
|
||||||
- "vitest*"
|
|
||||||
- "@vitest*"
|
|
||||||
tailwind:
|
|
||||||
applies-to: version-updates
|
|
||||||
patterns:
|
|
||||||
- "tailwindcss*"
|
|
||||||
- "@tailwindcss*"
|
|
||||||
|
|
||||||
# ui library
|
# ui library
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
open-pull-requests-limit: 99
|
open-pull-requests-limit: 99
|
||||||
|
|||||||
5
.github/file-filters.yml
vendored
5
.github/file-filters.yml
vendored
@ -4,11 +4,6 @@ ui: &ui
|
|||||||
- '.github/workflows/ui-*.yml'
|
- '.github/workflows/ui-*.yml'
|
||||||
- 'packages/ui/**/*'
|
- 'packages/ui/**/*'
|
||||||
|
|
||||||
maintenance: &maintenance
|
|
||||||
- '.github/workflows/maintenance-*.yml'
|
|
||||||
- 'maintenance/**/*'
|
|
||||||
- *ui
|
|
||||||
|
|
||||||
backend: &backend
|
backend: &backend
|
||||||
- '.github/workflows/test-backend.yml'
|
- '.github/workflows/test-backend.yml'
|
||||||
- 'backend/**/*'
|
- 'backend/**/*'
|
||||||
|
|||||||
2
.github/workflows/check-documentation.yml
vendored
2
.github/workflows/check-documentation.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check for markdown file changes
|
- name: Check for markdown file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
4
.github/workflows/deploy-documentation.yml
vendored
4
.github/workflows/deploy-documentation.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -38,7 +38,7 @@ jobs:
|
|||||||
run: npm install && npm run docs:build
|
run: npm install && npm run docs:build
|
||||||
|
|
||||||
- name: Deploy Vuepress to Github Pages
|
- name: Deploy Vuepress to Github Pages
|
||||||
uses: crazy-max/ghaction-github-pages@1d6ee9b181a81033a16bd707a1401afa978daab4 # v4.0.0
|
uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 # v4.0.0
|
||||||
with:
|
with:
|
||||||
target_branch: gh-pages
|
target_branch: gh-pages
|
||||||
build_dir: .vuepress/dist
|
build_dir: .vuepress/dist
|
||||||
|
|||||||
16
.github/workflows/docker-push.yml
vendored
16
.github/workflows/docker-push.yml
vendored
@ -37,15 +37,15 @@ jobs:
|
|||||||
target: production
|
target: production
|
||||||
- name: maintenance-base
|
- name: maintenance-base
|
||||||
context: .
|
context: .
|
||||||
file: maintenance/Dockerfile
|
file: webapp/Dockerfile.maintenance
|
||||||
target: production
|
target: base
|
||||||
- name: maintenance-build
|
- name: maintenance-build
|
||||||
context: .
|
context: .
|
||||||
file: maintenance/Dockerfile
|
file: webapp/Dockerfile.maintenance
|
||||||
target: build
|
target: build
|
||||||
- name: maintenance
|
- name: maintenance
|
||||||
context: .
|
context: .
|
||||||
file: maintenance/Dockerfile
|
file: webapp/Dockerfile.maintenance
|
||||||
target: production
|
target: production
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
@ -61,16 +61,16 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
|
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@030e881283bb7a6894de51c315a6bfe6a94e05cf
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
@ -83,7 +83,7 @@ jobs:
|
|||||||
type=sha
|
type=sha
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.app.context }}
|
context: ${{ matrix.app.context }}
|
||||||
target: ${{ matrix.app.target }}
|
target: ${{ matrix.app.target }}
|
||||||
|
|||||||
77
.github/workflows/maintenance-build.yml
vendored
77
.github/workflows/maintenance-build.yml
vendored
@ -1,77 +0,0 @@
|
|||||||
name: Maintenance Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: maintenance
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
name: Detect File Changes
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Check for file changes
|
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
filters: .github/file-filters.yml
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
if: needs.files-changed.outputs.maintenance == '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: 'maintenance/.tool-versions'
|
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: maintenance/package-lock.json
|
|
||||||
|
|
||||||
- name: Build UI library
|
|
||||||
working-directory: packages/ui
|
|
||||||
run: npm ci && npm run build
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Generate static site
|
|
||||||
run: npx nuxt generate
|
|
||||||
|
|
||||||
- name: Verify build output
|
|
||||||
run: |
|
|
||||||
if [ ! -d ".output/public" ]; then
|
|
||||||
echo "::error::.output/public directory not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f ".output/public/index.html" ]; then
|
|
||||||
echo "::error::index.html not found in build output"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Build output verified!"
|
|
||||||
ls -la .output/public/
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: maintenance-site
|
|
||||||
path: maintenance/.output/public/
|
|
||||||
retention-days: 7
|
|
||||||
59
.github/workflows/maintenance-docker.yml
vendored
59
.github/workflows/maintenance-docker.yml
vendored
@ -1,59 +0,0 @@
|
|||||||
name: Maintenance Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
name: Detect File Changes
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Check for file changes
|
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
filters: .github/file-filters.yml
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build Docker Image
|
|
||||||
if: needs.files-changed.outputs.maintenance == '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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
|
||||||
|
|
||||||
- name: Build development image
|
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./maintenance/Dockerfile
|
|
||||||
target: development
|
|
||||||
push: false
|
|
||||||
tags: ocelot-social/maintenance:development
|
|
||||||
cache-from: type=gha,scope=maintenance-development
|
|
||||||
cache-to: type=gha,mode=max,scope=maintenance-development
|
|
||||||
|
|
||||||
- name: Build production image
|
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./maintenance/Dockerfile
|
|
||||||
target: production
|
|
||||||
push: false
|
|
||||||
tags: ocelot-social/maintenance:latest
|
|
||||||
cache-from: type=gha,scope=maintenance-production
|
|
||||||
cache-to: type=gha,mode=max,scope=maintenance-production
|
|
||||||
58
.github/workflows/maintenance-lint.yml
vendored
58
.github/workflows/maintenance-lint.yml
vendored
@ -1,58 +0,0 @@
|
|||||||
name: Maintenance Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: maintenance
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
name: Detect File Changes
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Check for file changes
|
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
filters: .github/file-filters.yml
|
|
||||||
|
|
||||||
lint:
|
|
||||||
name: ESLint
|
|
||||||
if: needs.files-changed.outputs.maintenance == '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: 'maintenance/.tool-versions'
|
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: maintenance/package-lock.json
|
|
||||||
|
|
||||||
- name: Build UI library
|
|
||||||
working-directory: packages/ui
|
|
||||||
run: npm ci && npm run build
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run ESLint
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Run TypeScript type check
|
|
||||||
run: npx nuxi typecheck
|
|
||||||
63
.github/workflows/maintenance-test.yml
vendored
63
.github/workflows/maintenance-test.yml
vendored
@ -1,63 +0,0 @@
|
|||||||
name: Maintenance Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: maintenance
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
files-changed:
|
|
||||||
name: Detect File Changes
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
maintenance: ${{ steps.changes.outputs.maintenance }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Check for file changes
|
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
||||||
id: changes
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
filters: .github/file-filters.yml
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Unit Tests
|
|
||||||
if: needs.files-changed.outputs.maintenance == '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: 'maintenance/.tool-versions'
|
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: maintenance/package-lock.json
|
|
||||||
|
|
||||||
- name: Build UI library
|
|
||||||
working-directory: packages/ui
|
|
||||||
run: npm ci && npm run build
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run tests with coverage
|
|
||||||
run: npx vitest run --coverage
|
|
||||||
|
|
||||||
- name: Upload coverage report
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: maintenance-coverage-report
|
|
||||||
path: maintenance/coverage/
|
|
||||||
retention-days: 7
|
|
||||||
6
.github/workflows/publish.yml
vendored
6
.github/workflows/publish.yml
vendored
@ -72,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@1a91c28090489a711dd89a2424bb13d72a56e2e4 # 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
|
||||||
@ -80,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@1a91c28090489a711dd89a2424bb13d72a56e2e4 # 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
|
||||||
@ -88,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@1a91c28090489a711dd89a2424bb13d72a56e2e4 # 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
|
||||||
|
|||||||
18
.github/workflows/test-backend.yml
vendored
18
.github/workflows/test-backend.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check for backend file changes
|
- name: Check for backend file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -31,10 +31,10 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: Neo4J | Build 'community' image
|
- name: Neo4J | Build 'community' image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: neo4j
|
context: neo4j
|
||||||
file: neo4j/Dockerfile
|
file: neo4j/Dockerfile
|
||||||
@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
id: cache-neo4j
|
id: cache-neo4j
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
@ -64,10 +64,10 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: backend | Build 'test' image
|
- name: backend | Build 'test' image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: backend
|
context: backend
|
||||||
file: backend/Dockerfile
|
file: backend/Dockerfile
|
||||||
@ -82,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
id: cache-backend
|
id: cache-backend
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
@ -118,14 +118,14 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore Neo4J cache
|
- name: Restore Neo4J cache
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
|
|||||||
39
.github/workflows/test-e2e.yml
vendored
39
.github/workflows/test-e2e.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: Copy backend env file
|
- name: Copy backend env file
|
||||||
run: |
|
run: |
|
||||||
@ -19,7 +19,7 @@ jobs:
|
|||||||
cp webapp/.env.template webapp/.env
|
cp webapp/.env.template webapp/.env
|
||||||
|
|
||||||
- name: Neo4J | Build image
|
- name: Neo4J | Build image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: neo4j
|
context: neo4j
|
||||||
file: neo4j/Dockerfile
|
file: neo4j/Dockerfile
|
||||||
@ -30,7 +30,7 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max,scope=neo4j
|
cache-to: type=gha,mode=max,scope=neo4j
|
||||||
|
|
||||||
- name: Backend | Build image
|
- name: Backend | Build image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: backend
|
context: backend
|
||||||
file: backend/Dockerfile
|
file: backend/Dockerfile
|
||||||
@ -55,7 +55,7 @@ jobs:
|
|||||||
docker save "maildev/maildev:latest" > /tmp/mailserver.tar
|
docker save "maildev/maildev:latest" > /tmp/mailserver.tar
|
||||||
|
|
||||||
- name: Cache docker images
|
- name: Cache docker images
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/tmp/backend.tar
|
/tmp/backend.tar
|
||||||
@ -73,10 +73,10 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: Webapp | Build 'test' image
|
- name: Webapp | Build 'test' image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: webapp/Dockerfile
|
file: webapp/Dockerfile
|
||||||
@ -90,7 +90,7 @@ jobs:
|
|||||||
run: docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
run: 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache docker image
|
- name: Cache docker image
|
||||||
|
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/opt/cucumber-json-formatter
|
/opt/cucumber-json-formatter
|
||||||
@ -149,7 +149,7 @@ jobs:
|
|||||||
- name: List feature files
|
- name: List feature files
|
||||||
id: list
|
id: list
|
||||||
run: |
|
run: |
|
||||||
FEATURES=$(cd cypress && find e2e/ -name "*.feature" -printf '%P\n' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
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
|
echo "features=$FEATURES" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
fullstack_tests:
|
fullstack_tests:
|
||||||
@ -175,7 +175,7 @@ jobs:
|
|||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Restore cypress cache
|
- name: Restore cypress cache
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/opt/cucumber-json-formatter
|
/opt/cucumber-json-formatter
|
||||||
@ -185,7 +185,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/tmp/backend.tar
|
/tmp/backend.tar
|
||||||
@ -196,7 +196,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
@ -233,12 +233,6 @@ jobs:
|
|||||||
timeout 120 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 5; done'
|
timeout 120 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 5; done'
|
||||||
echo "Webapp is ready."
|
echo "Webapp is ready."
|
||||||
|
|
||||||
- name: Initialize database
|
|
||||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn db:migrate init
|
|
||||||
|
|
||||||
- name: Migrate database
|
|
||||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn db:migrate up
|
|
||||||
|
|
||||||
- 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/e2e/${{ matrix.feature }}"
|
run: yarn run cypress:run --spec "cypress/e2e/${{ matrix.feature }}"
|
||||||
@ -249,17 +243,12 @@ jobs:
|
|||||||
cd cypress/
|
cd cypress/
|
||||||
node create-cucumber-html-report.js
|
node create-cucumber-html-report.js
|
||||||
|
|
||||||
- name: Full stack tests | if tests failed, compute artifact name
|
|
||||||
id: artifact-name
|
|
||||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
|
||||||
run: echo "name=e2e-report-$(echo '${{ matrix.feature }}' | tr '/' '-')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.artifact-name.outputs.name }}
|
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
|
||||||
|
|
||||||
e2e_status:
|
e2e_status:
|
||||||
|
|||||||
10
.github/workflows/test-webapp.yml
vendored
10
.github/workflows/test-webapp.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check for frontend file changes
|
- name: Check for frontend file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -50,10 +50,10 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: Webapp | Build 'test' image
|
- name: Webapp | Build 'test' image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: webapp/Dockerfile
|
file: webapp/Dockerfile
|
||||||
@ -67,7 +67,7 @@ jobs:
|
|||||||
run: docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar
|
run: 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
@ -103,7 +103,7 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore webapp cache
|
- name: Restore webapp cache
|
||||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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
|
||||||
|
|||||||
4
.github/workflows/ui-build.yml
vendored
4
.github/workflows/ui-build.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -88,7 +88,7 @@ jobs:
|
|||||||
run: npm run validate
|
run: npm run validate
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: packages/ui/dist/
|
path: packages/ui/dist/
|
||||||
|
|||||||
6
.github/workflows/ui-compatibility.yml
vendored
6
.github/workflows/ui-compatibility.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: ui-dist
|
name: ui-dist
|
||||||
path: packages/ui/dist/
|
path: packages/ui/dist/
|
||||||
@ -78,7 +78,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ui-dist
|
name: ui-dist
|
||||||
path: packages/ui/dist/
|
path: packages/ui/dist/
|
||||||
|
|||||||
8
.github/workflows/ui-docker.yml
vendored
8
.github/workflows/ui-docker.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -34,10 +34,10 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
|
||||||
- name: Build development image
|
- name: Build development image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: ./packages/ui
|
context: ./packages/ui
|
||||||
file: ./packages/ui/Dockerfile
|
file: ./packages/ui/Dockerfile
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max,scope=ui-development
|
cache-to: type=gha,mode=max,scope=ui-development
|
||||||
|
|
||||||
- name: Build production image
|
- name: Build production image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: ./packages/ui
|
context: ./packages/ui
|
||||||
file: ./packages/ui/Dockerfile
|
file: ./packages/ui/Dockerfile
|
||||||
|
|||||||
2
.github/workflows/ui-lint.yml
vendored
2
.github/workflows/ui-lint.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
2
.github/workflows/ui-size.yml
vendored
2
.github/workflows/ui-size.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
4
.github/workflows/ui-storybook.yml
vendored
4
.github/workflows/ui-storybook.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -67,7 +67,7 @@ jobs:
|
|||||||
echo "✓ Storybook build verified!"
|
echo "✓ Storybook build verified!"
|
||||||
|
|
||||||
- name: Upload Storybook artifacts
|
- name: Upload Storybook artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: storybook-static
|
name: storybook-static
|
||||||
path: packages/ui/storybook-static/
|
path: packages/ui/storybook-static/
|
||||||
|
|||||||
4
.github/workflows/ui-test.yml
vendored
4
.github/workflows/ui-test.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -51,7 +51,7 @@ jobs:
|
|||||||
run: npm run test:coverage
|
run: npm run test:coverage
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report
|
||||||
|
|||||||
2
.github/workflows/ui-verify.yml
vendored
2
.github/workflows/ui-verify.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
4
.github/workflows/ui-visual.yml
vendored
4
.github/workflows/ui-visual.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
@ -54,7 +54,7 @@ jobs:
|
|||||||
run: npm run test:visual
|
run: npm run test:visual
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: visual-test-results
|
name: visual-test-results
|
||||||
|
|||||||
178
CHANGELOG.md
178
CHANGELOG.md
@ -4,186 +4,8 @@ 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.15.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.15.0...3.15.1)
|
|
||||||
|
|
||||||
- fix(webapp): fix dependency problem - async-validator required in production [`#9436`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9436)
|
|
||||||
|
|
||||||
#### [3.15.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.14.1...3.15.0)
|
|
||||||
|
|
||||||
> 24 March 2026
|
|
||||||
|
|
||||||
- chore(release): v3.15.0 [`#9434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9434)
|
|
||||||
- fix(backend): fix flaky backend tests [`#9433`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9433)
|
|
||||||
- refactor(webapp): remove styleguide [`#9432`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9432)
|
|
||||||
- feat(package/ui): os-menu [`#9431`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9431)
|
|
||||||
- refactor(webapp): migrate ds-select to OcelotSelect [`#9430`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9430)
|
|
||||||
- refactor(webapp): replace ds-icon with OsIcon and add missing svgs [`#9429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9429)
|
|
||||||
- refactor(webapp): migrate ds-input to OcelotInput [`#9428`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9428)
|
|
||||||
- build(deps): bump graphql from 16.13.0 to 16.13.1 in /backend [`#9354`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9354)
|
|
||||||
- build(deps): bump sanitize-html from 2.17.1 to 2.17.2 in /backend [`#9420`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9420)
|
|
||||||
- build(deps-dev): bump jsdom from 28.1.0 to 29.0.1 in /packages/ui [`#9413`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9413)
|
|
||||||
- build(deps): bump actions/cache from 5.0.3 to 5.0.4 [`#9410`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9410)
|
|
||||||
- build(deps-dev): bump @tailwindcss/vite from 4.2.1 to 4.2.2 in /packages/ui [`#9419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9419)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.19 to 10.3.1 in /packages/ui [`#9418`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9418)
|
|
||||||
- build(deps): bump ioredis from 5.10.0 to 5.10.1 in /backend [`#9425`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9425)
|
|
||||||
- build(deps-dev): bump @tailwindcss/cli from 4.2.1 to 4.2.2 in /packages/ui [`#9421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9421)
|
|
||||||
- build(deps-dev): bump tailwindcss from 4.2.1 to 4.2.2 in /packages/ui [`#9422`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9422)
|
|
||||||
- build(deps-dev): bump eslint-plugin-playwright from 2.9.0 to 2.10.1 in /packages/ui [`#9424`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9424)
|
|
||||||
- build(deps): bump node from 25.8.0-alpine to 25.8.1-alpine in /backend [`#9377`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9377)
|
|
||||||
- build(deps): bump node from 25.8.0-alpine to 25.8.1-alpine in /webapp [`#9378`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9378)
|
|
||||||
- build(deps): bump the metascraper group in /backend with 12 updates [`#9384`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9384)
|
|
||||||
- build(deps): bump dorny/paths-filter from 3.0.2 to 4.0.1 [`#9380`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9380)
|
|
||||||
- build(deps): bump nodemailer from 8.0.1 to 8.0.2 in /backend [`#9388`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9388)
|
|
||||||
- build(deps-dev): bump @types/node from 25.4.0 to 25.5.0 in /backend [`#9391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9391)
|
|
||||||
- build(deps): bump pug from 3.0.3 to 3.0.4 in /backend [`#9385`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9385)
|
|
||||||
- build(deps): bump nginx from 1.29.5-alpine to 1.29.6-alpine in /webapp [`#9379`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9379)
|
|
||||||
- fix(package/ui): override active hover effect of disabled button to not create visual glitches when button state changes [`#9408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9408)
|
|
||||||
- refactor(webapp): vue 3 migration - ds-form [`#9407`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9407)
|
|
||||||
- build(deps): bump slugify from 1.6.6 to 1.6.8 in /backend [`#9390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9390)
|
|
||||||
- build(deps-dev): bump the vitest group in /packages/ui with 2 updates [`#9394`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9394)
|
|
||||||
- build(deps-dev): bump @types/node from 25.3.5 to 25.5.0 in /packages/ui [`#9396`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9396)
|
|
||||||
- build(deps-dev): bump jest from 30.2.0 to 30.3.0 in /backend [`#9398`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9398)
|
|
||||||
- build(deps-dev): bump eslint-plugin-jsdoc from 62.7.1 to 62.8.0 in /packages/ui [`#9399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9399)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.17 to 10.2.19 in /packages/ui [`#9400`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9400)
|
|
||||||
- build(deps-dev): bump eslint-plugin-storybook from 10.2.17 to 10.2.19 in /packages/ui [`#9401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9401)
|
|
||||||
- fix(webapp): fix flaky e2e test [`#9404`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9404)
|
|
||||||
- refactor(webapp): ds-radio -> html [`#9403`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9403)
|
|
||||||
- fix(webapp): fix search + search e2e [`#9376`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9376)
|
|
||||||
- feat(package/ui): os-modal & webapp integration [`#9375`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9375)
|
|
||||||
- refactor(webapp): pin and unpin network-wide clarification [`#9374`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9374)
|
|
||||||
- feat(backend): translate all emails into missing languages [`#9372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9372)
|
|
||||||
- fix(webapp): fix distance of spinner in feed [`#9373`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9373)
|
|
||||||
- feat(webapp): ukrainian language translation [`#9371`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9371)
|
|
||||||
- fix(webapp): fix date select language crash [`#9370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9370)
|
|
||||||
- fix(webapp): fix user teaser group name color (now grey) [`#9367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9367)
|
|
||||||
- fix(package/ui): update eslint-config-it4c & fix lint errors [`#9368`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9368)
|
|
||||||
- build(deps): bump docker/login-action from 3.7.0 to 4.0.0 [`#9349`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9349)
|
|
||||||
- build(deps): bump crazy-max/ghaction-github-pages from 4.2.0 to 5.0.0 [`#9347`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9347)
|
|
||||||
- build(deps): bump docker/metadata-action from 5.10.0 to 6.0.0 [`#9345`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9345)
|
|
||||||
- build(deps): bump node from 25.7.0-alpine to 25.8.0-alpine in /backend [`#9343`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9343)
|
|
||||||
- build(deps): bump node from 25.7.0-alpine to 25.8.0-alpine in /webapp [`#9344`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9344)
|
|
||||||
- build(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0 [`#9348`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9348)
|
|
||||||
- build(deps): bump peter-evans/repository-dispatch from f49a8ac5751834a0666df77deb0289abbe2b3a78 to 11446b25a5fd252975d4bdf43d8989a5ac4f16c5 [`#9346`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9346)
|
|
||||||
- build(deps): bump docker/build-push-action from 6.19.2 to 7.0.0 [`#9350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9350)
|
|
||||||
- build(deps-dev): bump webpack from 5.105.3 to 5.105.4 [`#9351`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9351)
|
|
||||||
- build(deps-dev): bump @types/node from 25.3.2 to 25.3.5 in /backend [`#9352`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9352)
|
|
||||||
- build(deps-dev): bump eslint-plugin-playwright from 2.8.0 to 2.9.0 in /packages/ui [`#9358`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9358)
|
|
||||||
- build(deps): bump @aws-sdk/client-s3 from 3.1000.0 to 3.1004.0 in /backend [`#9356`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9356)
|
|
||||||
- build(deps-dev): bump @size-limit/file from 12.0.0 to 12.0.1 in /packages/ui [`#9359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9359)
|
|
||||||
- build(deps-dev): bump eslint-plugin-storybook from 10.2.13 to 10.2.16 in /packages/ui [`#9360`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9360)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.13 to 10.2.16 in /packages/ui [`#9361`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9361)
|
|
||||||
- build(deps-dev): bump storybook from 10.2.13 to 10.2.16 in /packages/ui [`#9362`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9362)
|
|
||||||
- refactor(docker): mount styleguide and packages/ui in docker-compose.override.yml [`#9342`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9342)
|
|
||||||
- build(deps-dev): bump publint from 0.3.17 to 0.3.18 in /packages/ui [`#9364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9364)
|
|
||||||
- build(deps-dev): bump @types/node from 25.3.2 to 25.3.5 in /packages/ui [`#9366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9366)
|
|
||||||
- build(deps): bump node from 25.6.1-alpine to 25.7.0-alpine in /backend [`#9304`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9304)
|
|
||||||
- build(deps): bump node from 25.6.1-alpine to 25.7.0-alpine in /webapp [`#9305`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9305)
|
|
||||||
- build(deps): bump actions/download-artifact from 4 to 8 [`#9306`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9306)
|
|
||||||
- fix(backend): ensure req.body exists with global parser setup [`#9340`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9340)
|
|
||||||
- fix(webapp): remove flags from locales [`#9341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9341)
|
|
||||||
- build(deps): bump actions/upload-artifact from 6 to 7 [`#9307`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9307)
|
|
||||||
- refactor(e2e): remove e2e relics [`#9336`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9336)
|
|
||||||
- feat(backend): smtp - new config variable to allow ignoring tls errors [`#9339`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9339)
|
|
||||||
- build(deps): bump ioredis from 5.9.3 to 5.10.0 in /backend [`#9308`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9308)
|
|
||||||
- fix(webapp): meta - description [`#9338`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9338)
|
|
||||||
- fix(webapp): fix lang query location [`#9337`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9337)
|
|
||||||
- feat(webapp): metadata for link preview [`#9335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9335)
|
|
||||||
- build(deps-dev): bump the cypress group with 2 updates [`#9309`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9309)
|
|
||||||
- build(deps-dev): bump webpack from 5.105.2 to 5.105.3 [`#9311`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9311)
|
|
||||||
- build(deps): bump minimatch from 10.2.2 to 10.2.4 in /backend [`#9313`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9313)
|
|
||||||
- build(deps-dev): bump @types/lodash from 4.17.23 to 4.17.24 in /backend [`#9314`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9314)
|
|
||||||
- build(deps-dev): bump the vue group in /packages/ui with 2 updates [`#9315`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9315)
|
|
||||||
- build(deps): bump @aws-sdk/lib-storage from 3.990.0 to 3.995.0 in /backend [`#9316`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9316)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.10 to 10.2.13 in /packages/ui [`#9318`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9318)
|
|
||||||
- build(deps-dev): bump eslint-plugin-jsdoc from 62.7.0 to 62.7.1 in /packages/ui [`#9320`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9320)
|
|
||||||
- build(deps-dev): bump @types/node from 25.3.0 to 25.3.2 in /backend [`#9323`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9323)
|
|
||||||
- build(deps-dev): bump @tailwindcss/vite from 4.2.0 to 4.2.1 in /packages/ui [`#9325`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9325)
|
|
||||||
- build(deps): bump graphql from 16.12.0 to 16.13.0 in /backend [`#9324`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9324)
|
|
||||||
- build(deps-dev): bump @types/node from 25.3.0 to 25.3.2 in /packages/ui [`#9328`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9328)
|
|
||||||
- build(deps-dev): bump eslint-plugin-playwright from 2.7.0 to 2.8.0 in /packages/ui [`#9329`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9329)
|
|
||||||
- build(deps-dev): bump eslint-plugin-storybook from 10.2.10 to 10.2.13 in /packages/ui [`#9330`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9330)
|
|
||||||
- build(deps): bump @aws-sdk/client-s3 from 3.995.0 to 3.1000.0 in /backend [`#9331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9331)
|
|
||||||
- build(deps-dev): bump @tailwindcss/cli from 4.2.0 to 4.2.1 in /packages/ui [`#9332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9332)
|
|
||||||
- build(deps-dev): bump tailwindcss from 4.2.0 to 4.2.1 in /packages/ui [`#9333`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9333)
|
|
||||||
- fix(workflow): ensure cucumber-json-formatter [`#9300`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9300)
|
|
||||||
- feat(e2e): e2e - chat notification [`#9303`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9303)
|
|
||||||
- feat(webapp): complete translations + Albanian [`#9301`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9301)
|
|
||||||
- fix(webapp): downgrade graphql - socket not working [`#9302`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9302)
|
|
||||||
- fix(webapp): fix landscape image distances [`#9299`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9299)
|
|
||||||
- build(deps): bump graphql from 14.7.0 to 16.12.0 in /webapp [`#9045`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9045)
|
|
||||||
- feat(webapp): first draft of landscape mode [`#9298`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9298)
|
|
||||||
- fix(webapp): fix time display in user teaser [`#9297`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9297)
|
|
||||||
- fix(webapp): fix embed in non-editor-mode [`#9296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9296)
|
|
||||||
- build(deps): bump body-parser from 1.20.3 to 2.2.2 in /backend [`#9101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9101)
|
|
||||||
- refactor(workflow): use docker cache [`#9294`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9294)
|
|
||||||
- build(deps-dev): bump @badeball/cypress-cucumber-preprocessor from 24.0.0 to 24.0.1 in the cypress group [`#9256`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9256)
|
|
||||||
- refactor(backend): graphql lint + query gql files [`#9293`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9293)
|
|
||||||
- build(deps-dev): bump @tailwindcss/vite from 4.1.18 to 4.2.0 in /packages/ui [`#9257`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9257)
|
|
||||||
- build(deps): bump tailwind-merge from 3.4.0 to 3.5.0 in /packages/ui [`#9258`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9258)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.8 to 10.2.10 in /packages/ui [`#9259`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9259)
|
|
||||||
- build(deps-dev): bump @types/node from 25.2.3 to 25.3.0 in /packages/ui [`#9261`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9261)
|
|
||||||
- build(deps): bump the metascraper group in /backend with 12 updates [`#9262`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9262)
|
|
||||||
- build(deps-dev): bump nodemon from 3.1.11 to 3.1.14 in /backend [`#9265`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9265)
|
|
||||||
- refactor(backend): update apollo [`#9292`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9292)
|
|
||||||
- build(deps-dev): bump @tailwindcss/cli from 4.1.18 to 4.2.0 in /packages/ui [`#9263`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9263)
|
|
||||||
- build(deps-dev): bump jsdom from 28.0.0 to 28.1.0 in /packages/ui [`#9267`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9267)
|
|
||||||
- refactor(backend): use eslint config it4c [`#9290`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9290)
|
|
||||||
- build(deps-dev): bump eslint-plugin-jsdoc from 62.5.4 to 62.7.0 in /packages/ui [`#9271`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9271)
|
|
||||||
- build(deps-dev): bump tailwindcss from 4.1.18 to 4.2.0 in /packages/ui [`#9269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9269)
|
|
||||||
- build(deps-dev): bump eslint-plugin-playwright from 2.5.1 to 2.7.0 in /packages/ui [`#9264`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9264)
|
|
||||||
- build(deps-dev): bump glob from 13.0.3 to 13.0.6 in /packages/ui [`#9272`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9272)
|
|
||||||
- build(deps-dev): bump eslint-plugin-storybook from 10.2.8 to 10.2.10 in /packages/ui [`#9274`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9274)
|
|
||||||
- build(deps): bump @aws-sdk/client-s3 from 3.990.0 to 3.995.0 in /backend [`#9275`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9275)
|
|
||||||
- fix(other): fix image cache [`#9289`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9289)
|
|
||||||
- build(deps): bump sanitize-html from 2.17.0 to 2.17.1 in /backend [`#9276`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9276)
|
|
||||||
- build(deps): bump @aws-sdk/lib-storage from 3.985.0 to 3.990.0 in /backend [`#9277`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9277)
|
|
||||||
- build(deps-dev): bump @types/node from 25.2.3 to 25.3.0 in /backend [`#9278`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9278)
|
|
||||||
- fix(webapp): fix badge select + drag&drop for badges on desktop devices [`#9287`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9287)
|
|
||||||
- feat(webapp): feed view mode [`#9285`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9285)
|
|
||||||
- fix(webapp): flex layout und spacing in profile und group pages [`#9286`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9286)
|
|
||||||
- build(deps): bump minimatch from 10.2.0 to 10.2.2 in /backend [`#9279`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9279)
|
|
||||||
- build(deps-dev): bump eslint-plugin-jest from 29.14.0 to 29.15.0 in /backend [`#9280`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9280)
|
|
||||||
- fix(webapp): optimize masonry grid rendering and add SSR compatibility [`#9284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9284)
|
|
||||||
- fix(webapp): add responsive masonry layout and skeleton loading UI [`#9282`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9282)
|
|
||||||
- fix(webapp): user teaser [`#9283`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9283)
|
|
||||||
- fix(webapp): add responsive mobile menu with locale switching and filter support [`#9281`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9281)
|
|
||||||
- fix(webapp): tab navigation [`#9255`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9255)
|
|
||||||
- feat(package/ui): os-number [`#9254`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9254)
|
|
||||||
- refactor(webapp): webapp test - no more skipped, no more todos [`#9252`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9252)
|
|
||||||
- refactor(webapp): ds-text equivalent css [`#9253`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9253)
|
|
||||||
- refactor(webapp): ds-table to plain html [`#9251`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9251)
|
|
||||||
- feat(package/ui): os-badge [`#9250`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9250)
|
|
||||||
- refactor(webapp): improve webapp build [`#9249`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9249)
|
|
||||||
- refactor(webapp): ds-grid [`#9248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9248)
|
|
||||||
- refactor(webapp): ds html [`#9247`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9247)
|
|
||||||
- refactor(package/ui): os-card [`#9246`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9246)
|
|
||||||
- refactor(package/ui): os-spinner [`#9245`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9245)
|
|
||||||
- fix(backend): fix memory leaks [`#9239`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9239)
|
|
||||||
- feat(webapp): more button icons, more loading states [`#9243`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9243)
|
|
||||||
- fix(webapp): properly autohide dropdown menu [`#9244`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9244)
|
|
||||||
- feat(package/ui): os-button suffix slot [`#9242`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9242)
|
|
||||||
- feat(webapp): push to top indicator [`#9237`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9237)
|
|
||||||
- refactor(webapp): migrate icons [`#9238`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9238)
|
|
||||||
- build(deps-dev): bump multiple-cucumber-html-reporter from 3.9.3 to 3.10.0 in the cypress group [`#9217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9217)
|
|
||||||
- fix(workflow): disable docstring [`#9236`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9236)
|
|
||||||
- feat(package/ui): os-icon [`#9234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9234)
|
|
||||||
- refactor(webapp): vue3 migration os button as prop, remove obsolete buttons & inline single user buttons [`#9214`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9214)
|
|
||||||
- build(deps): bump node from 25.6.0-alpine to 25.6.1-alpine in /webapp [`#9215`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9215)
|
|
||||||
- build(deps): bump node from 25.6.0-alpine to 25.6.1-alpine in /backend [`#9216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9216)
|
|
||||||
- build(deps-dev): bump webpack from 5.105.0 to 5.105.2 [`#9218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9218)
|
|
||||||
- fix(docker): fix some broken compose vars [`#9235`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9235)
|
|
||||||
- build(deps-dev): bump dotenv from 17.2.4 to 17.3.1 [`#9219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9219)
|
|
||||||
- build(deps-dev): bump eslint-plugin-jest from 29.13.0 to 29.14.0 in /backend [`#9221`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9221)
|
|
||||||
- build(deps): bump minimatch from 10.1.2 to 10.2.0 in /backend [`#9222`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9222)
|
|
||||||
- build(deps): bump ioredis from 5.9.2 to 5.9.3 in /backend [`#9223`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9223)
|
|
||||||
- build(deps-dev): bump vite-tsconfig-paths from 6.1.0 to 6.1.1 in /packages/ui in the vite group [`#9225`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9225)
|
|
||||||
- build(deps-dev): bump @storybook/vue3-vite from 10.2.7 to 10.2.8 in /packages/ui [`#9229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9229)
|
|
||||||
|
|
||||||
#### [3.14.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.14.0...3.14.1)
|
#### [3.14.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.14.0...3.14.1)
|
||||||
|
|
||||||
> 14 February 2026
|
|
||||||
|
|
||||||
- chore(release): v3.14.1 [`#9213`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9213)
|
|
||||||
- refactor(package/ui): eslint config it4c update [`#9233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9233)
|
- 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): 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-vuejs-accessibility from 2.4.1 to 2.5.0 in /packages/ui [`#9226`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/9226)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:25.8.2-alpine AS base
|
FROM node:25.6.1-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"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social-backend",
|
"name": "ocelot-social-backend",
|
||||||
"version": "3.15.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",
|
||||||
@ -32,8 +32,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.11.3",
|
"@apollo/server": "^4.11.3",
|
||||||
"@aws-sdk/client-s3": "^3.1005.0",
|
"@aws-sdk/client-s3": "^3.1000.0",
|
||||||
"@aws-sdk/lib-storage": "^3.1000.0",
|
"@aws-sdk/lib-storage": "^3.990.0",
|
||||||
"@graphql-tools/load-files": "^7.0.0",
|
"@graphql-tools/load-files": "^7.0.0",
|
||||||
"@graphql-tools/merge": "^9.0.0",
|
"@graphql-tools/merge": "^9.0.0",
|
||||||
"@sentry/node": "^5.30.0",
|
"@sentry/node": "^5.30.0",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"dotenv": "~17.0.1",
|
"dotenv": "~17.0.1",
|
||||||
"email-templates": "^13.0.1",
|
"email-templates": "^13.0.1",
|
||||||
"express": "^4.22.1",
|
"express": "^4.22.1",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.11.0",
|
||||||
"graphql-middleware": "~6.1.35",
|
"graphql-middleware": "~6.1.35",
|
||||||
"graphql-redis-subscriptions": "^2.7.0",
|
"graphql-redis-subscriptions": "^2.7.0",
|
||||||
"graphql-shield": "^7.6.5",
|
"graphql-shield": "^7.6.5",
|
||||||
@ -53,46 +53,46 @@
|
|||||||
"graphql-upload": "^13.0.0",
|
"graphql-upload": "^13.0.0",
|
||||||
"graphql-ws": "^5.16.2",
|
"graphql-ws": "^5.16.2",
|
||||||
"helmet": "~8.1.0",
|
"helmet": "~8.1.0",
|
||||||
"ioredis": "^5.10.1",
|
"ioredis": "^5.9.3",
|
||||||
"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.23",
|
"lodash": "~4.17.23",
|
||||||
"metascraper": "^5.50.0",
|
"metascraper": "^5.49.24",
|
||||||
"metascraper-author": "^5.50.0",
|
"metascraper-author": "^5.49.24",
|
||||||
"metascraper-date": "^5.50.0",
|
"metascraper-date": "^5.49.24",
|
||||||
"metascraper-description": "^5.50.0",
|
"metascraper-description": "^5.49.24",
|
||||||
"metascraper-image": "^5.50.0",
|
"metascraper-image": "^5.49.24",
|
||||||
"metascraper-lang": "^5.50.0",
|
"metascraper-lang": "^5.49.24",
|
||||||
"metascraper-lang-detector": "^4.10.2",
|
"metascraper-lang-detector": "^4.10.2",
|
||||||
"metascraper-logo": "^5.50.0",
|
"metascraper-logo": "^5.49.24",
|
||||||
"metascraper-publisher": "^5.50.0",
|
"metascraper-publisher": "^5.49.24",
|
||||||
"metascraper-soundcloud": "^5.34.4",
|
"metascraper-soundcloud": "^5.34.4",
|
||||||
"metascraper-title": "^5.50.0",
|
"metascraper-title": "^5.49.24",
|
||||||
"metascraper-url": "^5.50.0",
|
"metascraper-url": "^5.49.24",
|
||||||
"metascraper-video": "^5.50.0",
|
"metascraper-video": "^5.49.24",
|
||||||
"metascraper-youtube": "^5.50.0",
|
"metascraper-youtube": "^5.49.24",
|
||||||
"migrate": "^2.1.0",
|
"migrate": "^2.1.0",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"minimatch": "^10.2.4",
|
"minimatch": "^10.2.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": "^8.0.4",
|
"nodemailer": "^8.0.1",
|
||||||
"nodemailer-html-to-text": "^3.2.0",
|
"nodemailer-html-to-text": "^3.2.0",
|
||||||
"preview-email": "^3.1.1",
|
"preview-email": "^3.1.1",
|
||||||
"pug": "^3.0.4",
|
"pug": "^3.0.3",
|
||||||
"sanitize-html": "~2.17.2",
|
"sanitize-html": "~2.17.1",
|
||||||
"slugify": "^1.6.8",
|
"slugify": "^1.6.6",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"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.26",
|
"validator": "^13.15.26",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.18.2",
|
||||||
"xregexp": "^5.1.2"
|
"xregexp": "^5.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -100,15 +100,15 @@
|
|||||||
"@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.24",
|
"@types/lodash": "^4.17.23",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.3.0",
|
||||||
"@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",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.27.0",
|
||||||
"eslint-config-it4c": "^0.12.0",
|
"eslint-config-it4c": "^0.12.0",
|
||||||
"jest": "^30.3.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "~3.1.14",
|
"nodemon": "~3.1.14",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"rosie": "^2.1.1",
|
"rosie": "^2.1.1",
|
||||||
|
|||||||
@ -54,7 +54,6 @@ const SMTP_DKIM_KEYSELECTOR = env.SMTP_DKIM_KEYSELECTOR
|
|||||||
const SMTP_DKIM_PRIVATEKEY = env.SMTP_DKIM_PRIVATEKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break
|
const SMTP_DKIM_PRIVATEKEY = env.SMTP_DKIM_PRIVATEKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break
|
||||||
const SMTP_MAX_CONNECTIONS = (env.SMTP_MAX_CONNECTIONS && parseInt(env.SMTP_MAX_CONNECTIONS)) || 5
|
const SMTP_MAX_CONNECTIONS = (env.SMTP_MAX_CONNECTIONS && parseInt(env.SMTP_MAX_CONNECTIONS)) || 5
|
||||||
const SMTP_MAX_MESSAGES = (env.SMTP_MAX_MESSAGES && parseInt(env.SMTP_MAX_MESSAGES)) || 100
|
const SMTP_MAX_MESSAGES = (env.SMTP_MAX_MESSAGES && parseInt(env.SMTP_MAX_MESSAGES)) || 100
|
||||||
const SMTP_REJECT_UNAUTHORIZED = env.SMTP_REJECT_UNAUTHORIZED !== 'false' // default = true
|
|
||||||
|
|
||||||
const nodemailerTransportOptions: SMTPTransport.Options = {
|
const nodemailerTransportOptions: SMTPTransport.Options = {
|
||||||
host: SMTP_HOST,
|
host: SMTP_HOST,
|
||||||
@ -64,9 +63,6 @@ const nodemailerTransportOptions: SMTPTransport.Options = {
|
|||||||
pool: true,
|
pool: true,
|
||||||
maxConnections: SMTP_MAX_CONNECTIONS,
|
maxConnections: SMTP_MAX_CONNECTIONS,
|
||||||
maxMessages: SMTP_MAX_MESSAGES,
|
maxMessages: SMTP_MAX_MESSAGES,
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||||
nodemailerTransportOptions.auth = {
|
nodemailerTransportOptions.auth = {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
||||||
export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
|
export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
|
||||||
export const CHAT_MESSAGE_STATUS_UPDATED = 'CHAT_MESSAGE_STATUS_UPDATED'
|
|
||||||
export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED'
|
export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED'
|
||||||
|
|||||||
@ -40,7 +40,6 @@ export const getContext =
|
|||||||
req,
|
req,
|
||||||
cypherParams: {
|
cypherParams: {
|
||||||
currentUserId: user ? user.id : null,
|
currentUserId: user ? user.id : null,
|
||||||
languageDefault: config.LANGUAGE_DEFAULT.toUpperCase(),
|
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -199,6 +199,9 @@ Factory.define('post')
|
|||||||
// Convert false to null
|
// Convert false to null
|
||||||
return pinned || null
|
return pinned || null
|
||||||
})
|
})
|
||||||
|
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
|
||||||
|
return contentExcerpt || content
|
||||||
|
})
|
||||||
.attr('slug', ['slug', 'title'], (slug, title) => {
|
.attr('slug', ['slug', 'title'], (slug, title) => {
|
||||||
return slug || slugify(title, { lower: true })
|
return slug || slugify(title, { lower: true })
|
||||||
})
|
})
|
||||||
@ -227,55 +230,6 @@ Factory.define('post')
|
|||||||
return post
|
return post
|
||||||
})
|
})
|
||||||
|
|
||||||
Factory.define('group')
|
|
||||||
.option('ownerId', null)
|
|
||||||
.option('owner', ['ownerId'], (ownerId) => {
|
|
||||||
if (ownerId) return neode.find('User', ownerId)
|
|
||||||
return Factory.build('user')
|
|
||||||
})
|
|
||||||
.attrs({
|
|
||||||
id: uuid,
|
|
||||||
name: faker.company.name,
|
|
||||||
about: faker.lorem.sentence,
|
|
||||||
description: faker.lorem.paragraphs,
|
|
||||||
groupType: 'public',
|
|
||||||
actionRadius: 'regional',
|
|
||||||
deleted: false,
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
.attr('slug', ['slug', 'name'], (slug, name) => {
|
|
||||||
return slug || slugify(name, { lower: true })
|
|
||||||
})
|
|
||||||
.attr(
|
|
||||||
'descriptionExcerpt',
|
|
||||||
['descriptionExcerpt', 'description'],
|
|
||||||
(descriptionExcerpt, description) => {
|
|
||||||
return descriptionExcerpt || description
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.after(async (buildObject, options) => {
|
|
||||||
const [group, owner] = await Promise.all([neode.create('Group', buildObject), options.owner])
|
|
||||||
const session = driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((txc) =>
|
|
||||||
txc.run(
|
|
||||||
`
|
|
||||||
MATCH (owner:User {id: $ownerId}), (group:Group {id: $groupId})
|
|
||||||
MERGE (owner)-[:CREATED]->(group)
|
|
||||||
MERGE (owner)-[membership:MEMBER_OF]->(group)
|
|
||||||
SET membership.createdAt = toString(datetime()),
|
|
||||||
membership.updatedAt = toString(datetime()),
|
|
||||||
membership.role = 'owner'
|
|
||||||
`,
|
|
||||||
{ ownerId: owner.get('id'), groupId: buildObject.id },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
return group
|
|
||||||
})
|
|
||||||
|
|
||||||
Factory.define('comment')
|
Factory.define('comment')
|
||||||
.option('postId', null)
|
.option('postId', null)
|
||||||
.option('post', ['postId'], (postId) => {
|
.option('post', ['postId'], (postId) => {
|
||||||
@ -291,6 +245,9 @@ Factory.define('comment')
|
|||||||
id: uuid,
|
id: uuid,
|
||||||
content: faker.lorem.sentence,
|
content: faker.lorem.sentence,
|
||||||
})
|
})
|
||||||
|
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
|
||||||
|
return contentExcerpt || content
|
||||||
|
})
|
||||||
.after(async (buildObject, options) => {
|
.after(async (buildObject, options) => {
|
||||||
const [comment, author, post] = await Promise.all([
|
const [comment, author, post] = await Promise.all([
|
||||||
neode.create('Comment', buildObject),
|
neode.create('Comment', buildObject),
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
|
|
||||||
import { getDriver } from '@db/neo4j'
|
|
||||||
|
|
||||||
export const description =
|
|
||||||
'Replace global message.seen flag with per-user HAS_NOT_SEEN relationships'
|
|
||||||
|
|
||||||
export async function up(_next) {
|
|
||||||
const driver = getDriver()
|
|
||||||
const session = driver.session()
|
|
||||||
const transaction = session.beginTransaction()
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create HAS_NOT_SEEN relationships for unseen messages
|
|
||||||
// For each message with seen=false, create a relationship for each room member
|
|
||||||
// who is not the sender
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH (message:Message { seen: false })-[:INSIDE]->(room:Room)<-[:CHATS_IN]-(user:User)
|
|
||||||
WHERE NOT (user)-[:CREATED]->(message)
|
|
||||||
CREATE (user)-[:HAS_NOT_SEEN]->(message)
|
|
||||||
`)
|
|
||||||
|
|
||||||
// Remove the seen property from all messages
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH (m:Message)
|
|
||||||
REMOVE m.seen
|
|
||||||
`)
|
|
||||||
|
|
||||||
await transaction.commit()
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error)
|
|
||||||
await transaction.rollback()
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('rolled back')
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(_next) {
|
|
||||||
const driver = getDriver()
|
|
||||||
const session = driver.session()
|
|
||||||
const transaction = session.beginTransaction()
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Re-add seen property: messages with HAS_NOT_SEEN are unseen, rest are seen
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH (m:Message)
|
|
||||||
SET m.seen = true
|
|
||||||
`)
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH ()-[:HAS_NOT_SEEN]->(m:Message)
|
|
||||||
SET m.seen = false
|
|
||||||
`)
|
|
||||||
|
|
||||||
// Remove all HAS_NOT_SEEN relationships
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH ()-[r:HAS_NOT_SEEN]->(:Message)
|
|
||||||
DELETE r
|
|
||||||
`)
|
|
||||||
|
|
||||||
await transaction.commit()
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error)
|
|
||||||
await transaction.rollback()
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('rolled back')
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
|
|
||||||
import { getDriver } from '@db/neo4j'
|
|
||||||
|
|
||||||
export const description =
|
|
||||||
'Delete empty DM rooms (no messages) that were created by the old CreateRoom mutation'
|
|
||||||
|
|
||||||
export async function up(_next) {
|
|
||||||
const driver = getDriver()
|
|
||||||
const session = driver.session()
|
|
||||||
const transaction = session.beginTransaction()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH (room:Room)
|
|
||||||
WHERE NOT (room)-[:ROOM_FOR]->(:Group)
|
|
||||||
AND NOT (room)<-[:INSIDE]-(:Message)
|
|
||||||
DETACH DELETE room
|
|
||||||
`)
|
|
||||||
await transaction.commit()
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error)
|
|
||||||
await transaction.rollback()
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('rolled back')
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(_next) {
|
|
||||||
// Cannot restore deleted rooms
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
|
|
||||||
import { getDriver } from '@db/neo4j'
|
|
||||||
|
|
||||||
export const description = 'Remove contentExcerpt property from Post and Comment nodes'
|
|
||||||
|
|
||||||
export async function up(_next) {
|
|
||||||
const driver = getDriver()
|
|
||||||
const session = driver.session()
|
|
||||||
const transaction = session.beginTransaction()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transaction.run(`
|
|
||||||
MATCH (n)
|
|
||||||
WHERE n:Post OR n:Comment
|
|
||||||
REMOVE n.contentExcerpt
|
|
||||||
`)
|
|
||||||
await transaction.commit()
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error)
|
|
||||||
await transaction.rollback()
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('rolled back')
|
|
||||||
throw new Error(error)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function down(_next) {
|
|
||||||
throw new Error(
|
|
||||||
'Irreversible migration: contentExcerpt was removed and cannot be restored without regenerating from content',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -10,6 +10,7 @@ export default {
|
|||||||
default: () => new Date().toISOString(),
|
default: () => new Date().toISOString(),
|
||||||
},
|
},
|
||||||
content: { type: 'string', disallow: [null], min: 3 },
|
content: { type: 'string', disallow: [null], min: 3 },
|
||||||
|
contentExcerpt: { type: 'string', allow: [null] },
|
||||||
deleted: { type: 'boolean', default: false },
|
deleted: { type: 'boolean', default: false },
|
||||||
disabled: { type: 'boolean', default: false },
|
disabled: { type: 'boolean', default: false },
|
||||||
post: {
|
post: {
|
||||||
|
|||||||
@ -13,7 +13,6 @@ export default {
|
|||||||
nameNL: { type: 'string' },
|
nameNL: { type: 'string' },
|
||||||
namePL: { type: 'string' },
|
namePL: { type: 'string' },
|
||||||
nameRU: { type: 'string' },
|
nameRU: { type: 'string' },
|
||||||
nameSQ: { type: 'string' },
|
|
||||||
isIn: {
|
isIn: {
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'IS_IN',
|
relationship: 'IS_IN',
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export default {
|
|||||||
title: { type: 'string', disallow: [null], min: 3 },
|
title: { type: 'string', disallow: [null], min: 3 },
|
||||||
slug: { type: 'string', allow: [null], unique: 'true' },
|
slug: { type: 'string', allow: [null], unique: 'true' },
|
||||||
content: { type: 'string', disallow: [null], required: true, min: 3 },
|
content: { type: 'string', disallow: [null], required: true, min: 3 },
|
||||||
|
contentExcerpt: { type: 'string', allow: [null] },
|
||||||
deleted: { type: 'boolean', default: false },
|
deleted: { type: 'boolean', default: false },
|
||||||
disabled: { type: 'boolean', default: false },
|
disabled: { type: 'boolean', default: false },
|
||||||
clickedCount: { type: 'int', default: 0 },
|
clickedCount: { type: 'int', default: 0 },
|
||||||
|
|||||||
@ -15,8 +15,8 @@ import CreateComment from '@graphql/queries/comments/CreateComment.gql'
|
|||||||
import ChangeGroupMemberRole from '@graphql/queries/groups/ChangeGroupMemberRole.gql'
|
import ChangeGroupMemberRole from '@graphql/queries/groups/ChangeGroupMemberRole.gql'
|
||||||
import CreateGroup from '@graphql/queries/groups/CreateGroup.gql'
|
import CreateGroup from '@graphql/queries/groups/CreateGroup.gql'
|
||||||
import JoinGroup from '@graphql/queries/groups/JoinGroup.gql'
|
import JoinGroup from '@graphql/queries/groups/JoinGroup.gql'
|
||||||
import CreateGroupRoom from '@graphql/queries/messaging/CreateGroupRoom.gql'
|
|
||||||
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
||||||
|
import CreateRoom from '@graphql/queries/messaging/CreateRoom.gql'
|
||||||
import CreatePost from '@graphql/queries/posts/CreatePost.gql'
|
import CreatePost from '@graphql/queries/posts/CreatePost.gql'
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
|
||||||
@ -830,103 +830,15 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('seed', 'invitecodes')
|
console.log('seed', 'invitecodes')
|
||||||
|
|
||||||
// Peter invited the core users: Jenny, Bob, Huey
|
|
||||||
await Factory.build(
|
await Factory.build(
|
||||||
'inviteCode',
|
'inviteCode',
|
||||||
{ code: 'PETER1', comment: 'For Jenny' },
|
{
|
||||||
{ generatedBy: peterLustig },
|
code: 'ABCDEF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
generatedBy: jennyRostock,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'PETER2', comment: 'For Bob' },
|
|
||||||
{ generatedBy: peterLustig },
|
|
||||||
)
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'PETER3', comment: 'For Huey' },
|
|
||||||
{ generatedBy: peterLustig },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Jenny invited Dewey, Louie, Dagobert
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'JENNY1', comment: 'For Dewey' },
|
|
||||||
{ generatedBy: jennyRostock },
|
|
||||||
)
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'JENNY2', comment: 'For Louie' },
|
|
||||||
{ generatedBy: jennyRostock },
|
|
||||||
)
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'JENNY3', comment: 'For Dagobert' },
|
|
||||||
{ generatedBy: jennyRostock },
|
|
||||||
)
|
|
||||||
// Jenny's shared code (used by additional users)
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'ABCDEF', comment: 'Share link' },
|
|
||||||
{ generatedBy: jennyRostock },
|
|
||||||
)
|
|
||||||
// Jenny's unused code (still active)
|
|
||||||
await Factory.build('inviteCode', { code: 'JNEW01' }, { generatedBy: jennyRostock })
|
|
||||||
// Jenny's invalidated code (was used once, then deactivated)
|
|
||||||
await Factory.build(
|
|
||||||
'inviteCode',
|
|
||||||
{ code: 'JENNY0', comment: 'Old link', expiresAt: new Date().toISOString() },
|
|
||||||
{ generatedBy: jennyRostock },
|
|
||||||
)
|
|
||||||
// Jenny total: JENNY1, JENNY2, JENNY3, ABCDEF, JNEW01 (5 active) + JENNY0 (1 expired) = 6 codes
|
|
||||||
|
|
||||||
// Create REDEEMED and INVITED relationships via Cypher
|
|
||||||
const inviteSession = database.driver.session()
|
|
||||||
try {
|
|
||||||
await inviteSession.writeTransaction((txc) =>
|
|
||||||
txc.run(`
|
|
||||||
// Peter's invitations
|
|
||||||
MATCH (jenny:User {id: 'u3'}), (code1:InviteCode {code: 'PETER1'}), (peter:User {id: 'u1'})
|
|
||||||
MERGE (jenny)-[:REDEEMED {createdAt: toString(datetime())}]->(code1)
|
|
||||||
MERGE (peter)-[:INVITED {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
MERGE (jenny)-[:FOLLOWS {createdAt: toString(datetime())}]->(peter)
|
|
||||||
MERGE (peter)-[:FOLLOWS {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
WITH 1 AS dummy
|
|
||||||
MATCH (bob:User {id: 'u2'}), (code2:InviteCode {code: 'PETER2'}), (peter:User {id: 'u1'})
|
|
||||||
MERGE (bob)-[:REDEEMED {createdAt: toString(datetime())}]->(code2)
|
|
||||||
MERGE (peter)-[:INVITED {createdAt: toString(datetime())}]->(bob)
|
|
||||||
MERGE (bob)-[:FOLLOWS {createdAt: toString(datetime())}]->(peter)
|
|
||||||
MERGE (peter)-[:FOLLOWS {createdAt: toString(datetime())}]->(bob)
|
|
||||||
WITH 1 AS dummy
|
|
||||||
MATCH (huey:User {id: 'u4'}), (code3:InviteCode {code: 'PETER3'}), (peter:User {id: 'u1'})
|
|
||||||
MERGE (huey)-[:REDEEMED {createdAt: toString(datetime())}]->(code3)
|
|
||||||
MERGE (peter)-[:INVITED {createdAt: toString(datetime())}]->(huey)
|
|
||||||
MERGE (huey)-[:FOLLOWS {createdAt: toString(datetime())}]->(peter)
|
|
||||||
MERGE (peter)-[:FOLLOWS {createdAt: toString(datetime())}]->(huey)
|
|
||||||
WITH 1 AS dummy
|
|
||||||
// Jenny's invitations
|
|
||||||
MATCH (dewey:User {id: 'u5'}), (code4:InviteCode {code: 'JENNY1'}), (jenny:User {id: 'u3'})
|
|
||||||
MERGE (dewey)-[:REDEEMED {createdAt: toString(datetime())}]->(code4)
|
|
||||||
MERGE (jenny)-[:INVITED {createdAt: toString(datetime())}]->(dewey)
|
|
||||||
MERGE (dewey)-[:FOLLOWS {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
MERGE (jenny)-[:FOLLOWS {createdAt: toString(datetime())}]->(dewey)
|
|
||||||
WITH 1 AS dummy
|
|
||||||
MATCH (louie:User {id: 'u6'}), (code5:InviteCode {code: 'JENNY2'}), (jenny:User {id: 'u3'})
|
|
||||||
MERGE (louie)-[:REDEEMED {createdAt: toString(datetime())}]->(code5)
|
|
||||||
MERGE (jenny)-[:INVITED {createdAt: toString(datetime())}]->(louie)
|
|
||||||
MERGE (louie)-[:FOLLOWS {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
MERGE (jenny)-[:FOLLOWS {createdAt: toString(datetime())}]->(louie)
|
|
||||||
WITH 1 AS dummy
|
|
||||||
MATCH (dagobert:User {id: 'u7'}), (code6:InviteCode {code: 'JENNY3'}), (jenny:User {id: 'u3'})
|
|
||||||
MERGE (dagobert)-[:REDEEMED {createdAt: toString(datetime())}]->(code6)
|
|
||||||
MERGE (jenny)-[:INVITED {createdAt: toString(datetime())}]->(dagobert)
|
|
||||||
MERGE (dagobert)-[:FOLLOWS {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
MERGE (jenny)-[:FOLLOWS {createdAt: toString(datetime())}]->(dagobert)
|
|
||||||
`),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await inviteSession.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticatedUser = await louie.toJson()
|
authenticatedUser = await louie.toJson()
|
||||||
const mention1 =
|
const mention1 =
|
||||||
@ -1301,123 +1213,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('seed', 'users additional with map locations around Zwingenberg')
|
console.log('seed', 'users additional')
|
||||||
|
|
||||||
// Region Hessen (Mapbox-compatible hierarchy: place -> region -> country)
|
|
||||||
const Hessen = await Factory.build('location', {
|
|
||||||
id: 'region.8967011281068080',
|
|
||||||
name: 'Hessen',
|
|
||||||
type: 'region',
|
|
||||||
lng: 8.6528,
|
|
||||||
lat: 50.6521,
|
|
||||||
nameDE: 'Hessen',
|
|
||||||
nameEN: 'Hesse',
|
|
||||||
nameES: 'Hesse',
|
|
||||||
nameFR: 'Hesse',
|
|
||||||
nameIT: 'Assia',
|
|
||||||
namePT: 'Hessen',
|
|
||||||
nameNL: 'Hessen',
|
|
||||||
namePL: 'Hesja',
|
|
||||||
nameRU: 'Гессен',
|
|
||||||
})
|
|
||||||
await Hessen.relateTo(Germany, 'isIn')
|
|
||||||
|
|
||||||
// 50 villages around Zwingenberg (64673), Zwingenberg excluded
|
|
||||||
// Mapbox-compatible: type 'place', realistic IDs
|
|
||||||
const zwingenbergVillages = [
|
|
||||||
// Bergstraße (west)
|
|
||||||
{ id: 'place.8652241', name: 'Alsbach-Hähnlein', lat: 49.7389, lng: 8.6331 },
|
|
||||||
{ id: 'place.8652242', name: 'Bickenbach', lat: 49.7567, lng: 8.6178 },
|
|
||||||
{ id: 'place.8652243', name: 'Seeheim-Jugenheim', lat: 49.7631, lng: 8.6506 },
|
|
||||||
{ id: 'place.8652244', name: 'Bensheim', lat: 49.6812, lng: 8.6167 },
|
|
||||||
{ id: 'place.8652245', name: 'Auerbach', lat: 49.7053, lng: 8.6389 },
|
|
||||||
{ id: 'place.8652246', name: 'Heppenheim', lat: 49.6428, lng: 8.6392 },
|
|
||||||
{ id: 'place.8652247', name: 'Lorsch', lat: 49.6539, lng: 8.5678 },
|
|
||||||
{ id: 'place.8652248', name: 'Einhausen', lat: 49.6775, lng: 8.5578 },
|
|
||||||
{ id: 'place.8652249', name: 'Gernsheim', lat: 49.7528, lng: 8.4906 },
|
|
||||||
{ id: 'place.8652250', name: 'Pfungstadt', lat: 49.8056, lng: 8.6042 },
|
|
||||||
// Odenwald (east)
|
|
||||||
{ id: 'place.8652251', name: 'Reichenbach', lat: 49.725, lng: 8.67 },
|
|
||||||
{ id: 'place.8652252', name: 'Lautertal', lat: 49.7253, lng: 8.6914 },
|
|
||||||
{ id: 'place.8652253', name: 'Lindenfels', lat: 49.6836, lng: 8.7781 },
|
|
||||||
{ id: 'place.8652254', name: 'Modautal', lat: 49.7736, lng: 8.7258 },
|
|
||||||
{ id: 'place.8652255', name: 'Mühltal', lat: 49.8003, lng: 8.6917 },
|
|
||||||
{ id: 'place.8652256', name: 'Ober-Ramstadt', lat: 49.8306, lng: 8.7486 },
|
|
||||||
{ id: 'place.8652257', name: 'Reinheim', lat: 49.8289, lng: 8.8356 },
|
|
||||||
{ id: 'place.8652258', name: 'Groß-Bieberau', lat: 49.7906, lng: 8.8281 },
|
|
||||||
{ id: 'place.8652259', name: 'Fränkisch-Crumbach', lat: 49.745, lng: 8.8444 },
|
|
||||||
{ id: 'place.8652260', name: 'Brensbach', lat: 49.7742, lng: 8.8819 },
|
|
||||||
// Ried (west/southwest)
|
|
||||||
{ id: 'place.8652261', name: 'Bürstadt', lat: 49.6433, lng: 8.4506 },
|
|
||||||
{ id: 'place.8652262', name: 'Lampertheim', lat: 49.5978, lng: 8.47 },
|
|
||||||
{ id: 'place.8652263', name: 'Biblis', lat: 49.6878, lng: 8.4531 },
|
|
||||||
{ id: 'place.8652264', name: 'Groß-Rohrheim', lat: 49.7228, lng: 8.4822 },
|
|
||||||
{ id: 'place.8652265', name: 'Riedstadt', lat: 49.835, lng: 8.4944 },
|
|
||||||
{ id: 'place.8652266', name: 'Stockstadt am Rhein', lat: 49.8094, lng: 8.4656 },
|
|
||||||
{ id: 'place.8652267', name: 'Biebesheim', lat: 49.7806, lng: 8.4672 },
|
|
||||||
{ id: 'place.8652268', name: 'Trebur', lat: 49.9211, lng: 8.4081 },
|
|
||||||
{ id: 'place.8652269', name: 'Nauheim', lat: 49.9456, lng: 8.4494 },
|
|
||||||
{ id: 'place.8652270', name: 'Griesheim', lat: 49.8619, lng: 8.5722 },
|
|
||||||
// Darmstadt area (north)
|
|
||||||
{ id: 'place.8652271', name: 'Roßdorf', lat: 49.8572, lng: 8.7578 },
|
|
||||||
{ id: 'place.8652272', name: 'Messel', lat: 49.9333, lng: 8.75 },
|
|
||||||
{ id: 'place.8652273', name: 'Eppertshausen', lat: 49.95, lng: 8.85 },
|
|
||||||
{ id: 'place.8652274', name: 'Münster', lat: 49.9253, lng: 8.8653 },
|
|
||||||
{ id: 'place.8652275', name: 'Dieburg', lat: 49.8983, lng: 8.8467 },
|
|
||||||
{ id: 'place.8652276', name: 'Babenhausen', lat: 49.965, lng: 8.9511 },
|
|
||||||
{ id: 'place.8652277', name: 'Schaafheim', lat: 49.9244, lng: 8.9703 },
|
|
||||||
{ id: 'place.8652278', name: 'Groß-Umstadt', lat: 49.8667, lng: 8.9333 },
|
|
||||||
{ id: 'place.8652279', name: 'Otzberg', lat: 49.82, lng: 8.91 },
|
|
||||||
{ id: 'place.8652280', name: 'Höchst im Odenwald', lat: 49.7994, lng: 8.9986 },
|
|
||||||
// Further south
|
|
||||||
{ id: 'place.8652281', name: 'Mörlenbach', lat: 49.5969, lng: 8.7378 },
|
|
||||||
{ id: 'place.8652282', name: 'Rimbach', lat: 49.6256, lng: 8.7611 },
|
|
||||||
{ id: 'place.8652283', name: 'Fürth', lat: 49.6522, lng: 8.7789 },
|
|
||||||
{ id: 'place.8652284', name: 'Grasellenbach', lat: 49.6353, lng: 8.8531 },
|
|
||||||
{ id: 'place.8652285', name: 'Wald-Michelbach', lat: 49.57, lng: 8.83 },
|
|
||||||
{ id: 'place.8652286', name: 'Abtsteinach', lat: 49.5536, lng: 8.78 },
|
|
||||||
{ id: 'place.8652287', name: 'Gorxheimertal', lat: 49.5322, lng: 8.7322 },
|
|
||||||
{ id: 'place.8652288', name: 'Viernheim', lat: 49.5403, lng: 8.5783 },
|
|
||||||
{ id: 'place.8652289', name: 'Weinheim', lat: 49.5489, lng: 8.6639 },
|
|
||||||
{ id: 'place.8652290', name: 'Hemsbach', lat: 49.59, lng: 8.65 },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Create village location nodes (one per village, shared by all users in that village)
|
|
||||||
const villageLocationNodes: (typeof Hamburg)[] = []
|
|
||||||
for (const village of zwingenbergVillages) {
|
|
||||||
const location = await Factory.build('location', {
|
|
||||||
id: village.id,
|
|
||||||
name: village.name,
|
|
||||||
type: 'place',
|
|
||||||
lng: village.lng,
|
|
||||||
lat: village.lat,
|
|
||||||
nameDE: village.name,
|
|
||||||
nameEN: village.name,
|
|
||||||
nameES: village.name,
|
|
||||||
nameFR: village.name,
|
|
||||||
nameIT: village.name,
|
|
||||||
namePT: village.name,
|
|
||||||
nameNL: village.name,
|
|
||||||
namePL: village.name,
|
|
||||||
nameRU: village.name,
|
|
||||||
})
|
|
||||||
await location.relateTo(Hessen, 'isIn')
|
|
||||||
villageLocationNodes.push(location)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 1000 additional users with locations assigned during creation
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const additionalUsers: any[] = []
|
const additionalUsers: any[] = []
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
if (i % 100 === 0) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('seed', `additional users ${i}/1000`)
|
|
||||||
}
|
|
||||||
const user = await Factory.build('user')
|
const user = await Factory.build('user')
|
||||||
await jennyRostock.relateTo(user, 'following')
|
await jennyRostock.relateTo(user, 'following')
|
||||||
await user.relateTo(jennyRostock, 'following')
|
await user.relateTo(jennyRostock, 'following')
|
||||||
// Assign village location (round-robin across 50 villages = ~20 users per village)
|
|
||||||
await user.relateTo(villageLocationNodes[i % villageLocationNodes.length], 'isIn')
|
|
||||||
additionalUsers.push(user)
|
additionalUsers.push(user)
|
||||||
|
|
||||||
const userObj = await user.toJson()
|
const userObj = await user.toJson()
|
||||||
@ -1431,34 +1233,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('seed', 'additional users 1000/1000 done')
|
|
||||||
|
|
||||||
// Jenny's first 99 additional users all redeemed code ABCDEF
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('seed', 'invite redemptions for additional users')
|
|
||||||
const jennyInviteSession = database.driver.session()
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < Math.min(99, additionalUsers.length); i++) {
|
|
||||||
// eslint-disable-next-line security/detect-object-injection
|
|
||||||
const userObj = await additionalUsers[i].toJson()
|
|
||||||
const userId = userObj.id as string
|
|
||||||
await jennyInviteSession.writeTransaction((txc) =>
|
|
||||||
txc.run(
|
|
||||||
`
|
|
||||||
MATCH (user:User {id: $userId}), (inviteCode:InviteCode {code: 'ABCDEF'}), (jenny:User {id: 'u3'})
|
|
||||||
MERGE (user)-[:REDEEMED {createdAt: toString(datetime())}]->(inviteCode)
|
|
||||||
MERGE (jenny)-[:INVITED {createdAt: toString(datetime())}]->(user)
|
|
||||||
MERGE (user)-[:FOLLOWS {createdAt: toString(datetime())}]->(jenny)
|
|
||||||
MERGE (jenny)-[:FOLLOWS {createdAt: toString(datetime())}]->(user)
|
|
||||||
`,
|
|
||||||
{ userId },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await jennyInviteSession.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jenny users
|
// Jenny users
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
@ -1757,139 +1531,87 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('seed', 'chat')
|
console.log('seed', 'chat')
|
||||||
// DM chat: Huey <-> Peter (first message creates room via userId)
|
|
||||||
authenticatedUser = await huey.toJson()
|
authenticatedUser = await huey.toJson()
|
||||||
const { data: firstMsgHueyPeter } = await mutate({
|
const { data: roomHueyPeter } = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: (await peterLustig.toJson()).id,
|
userId: (await peterLustig.toJson()).id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
authenticatedUser = await huey.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: CreateMessage,
|
||||||
|
variables: {
|
||||||
|
roomId: roomHueyPeter?.CreateRoom.id,
|
||||||
content: faker.lorem.sentence(),
|
content: faker.lorem.sentence(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const roomIdHueyPeter = firstMsgHueyPeter?.CreateMessage.room.id
|
|
||||||
|
|
||||||
for (let i = 0; i < 29; i++) {
|
|
||||||
authenticatedUser = await peterLustig.toJson()
|
authenticatedUser = await peterLustig.toJson()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: { roomId: roomIdHueyPeter, content: faker.lorem.sentence() },
|
variables: {
|
||||||
})
|
roomId: roomHueyPeter?.CreateRoom.id,
|
||||||
authenticatedUser = await huey.toJson()
|
content: faker.lorem.sentence(),
|
||||||
await mutate({
|
},
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { roomId: roomIdHueyPeter, content: faker.lorem.sentence() },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DM chat: Huey <-> Jenny (first message creates room via userId)
|
|
||||||
authenticatedUser = await huey.toJson()
|
authenticatedUser = await huey.toJson()
|
||||||
const { data: firstMsgHueyJenny } = await mutate({
|
const { data: roomHueyJenny } = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: (await jennyRostock.toJson()).id,
|
userId: (await jennyRostock.toJson()).id,
|
||||||
content: faker.lorem.sentence(),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const roomIdHueyJenny = firstMsgHueyJenny?.CreateMessage.room.id
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
|
||||||
for (let i = 0; i < 999; i++) {
|
|
||||||
authenticatedUser = await jennyRostock.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { roomId: roomIdHueyJenny, content: faker.lorem.sentence() },
|
|
||||||
})
|
|
||||||
authenticatedUser = await huey.toJson()
|
authenticatedUser = await huey.toJson()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { roomId: roomIdHueyJenny, content: faker.lorem.sentence() },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DM chats: Jenny <-> additionalUsers
|
|
||||||
for (const user of additionalUsers.slice(0, 99)) {
|
|
||||||
authenticatedUser = await jennyRostock.toJson()
|
|
||||||
const { data: firstMsg } = await mutate({
|
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
userId: (await user.toJson()).id,
|
roomId: roomHueyJenny?.CreateRoom.id,
|
||||||
content: faker.lorem.sentence(),
|
content: faker.lorem.sentence(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const dmRoomId = firstMsg?.CreateMessage.room.id
|
authenticatedUser = await jennyRostock.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: CreateMessage,
|
||||||
|
variables: {
|
||||||
|
roomId: roomHueyJenny?.CreateRoom.id,
|
||||||
|
content: faker.lorem.sentence(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < 28; i++) {
|
for (const user of additionalUsers.slice(0, 99)) {
|
||||||
|
authenticatedUser = await jennyRostock.toJson()
|
||||||
|
const { data: room } = await mutate({
|
||||||
|
mutation: CreateRoom,
|
||||||
|
variables: {
|
||||||
|
userId: (await user.toJson()).id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 29; i++) {
|
||||||
|
authenticatedUser = await jennyRostock.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: CreateMessage,
|
||||||
|
variables: {
|
||||||
|
roomId: room?.CreateRoom.id,
|
||||||
|
content: faker.lorem.sentence(),
|
||||||
|
},
|
||||||
|
})
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await user.toJson()
|
||||||
await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { roomId: dmRoomId, content: faker.lorem.sentence() },
|
|
||||||
})
|
|
||||||
authenticatedUser = await jennyRostock.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { roomId: dmRoomId, content: faker.lorem.sentence() },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('seed', 'group chat')
|
|
||||||
|
|
||||||
// Group g1 (School For Citizens) - active members: Jenny(owner/creator), Peter(usual), Bob(usual), Dewey(admin), Louie(owner), Dagobert(usual)
|
|
||||||
// Create group room as Jenny (creator of g1)
|
|
||||||
authenticatedUser = await jennyRostock.toJson()
|
|
||||||
const { data: roomG1 } = await mutate({
|
|
||||||
mutation: CreateGroupRoom,
|
|
||||||
variables: { groupId: 'g1' },
|
|
||||||
})
|
|
||||||
const g1RoomId = roomG1?.CreateGroupRoom.id
|
|
||||||
|
|
||||||
// Members have a conversation
|
|
||||||
const g1Members = [
|
|
||||||
{ user: jennyRostock, name: 'Jenny' },
|
|
||||||
{ user: peterLustig, name: 'Peter' },
|
|
||||||
{ user: dewey, name: 'Dewey' },
|
|
||||||
{ user: louie, name: 'Louie' },
|
|
||||||
]
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
const member = g1Members[i % g1Members.length]
|
|
||||||
authenticatedUser = await member.user.toJson()
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
roomId: g1RoomId,
|
roomId: room?.CreateRoom.id,
|
||||||
content: faker.lorem.sentence(),
|
content: faker.lorem.sentence(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group g2 (Yoga Practice) - active members: Bob(owner/creator), Jenny(usual), Dewey(admin), Louie(usual), Dagobert(usual) - Huey is pending
|
|
||||||
authenticatedUser = await bobDerBaumeister.toJson()
|
|
||||||
const { data: roomG2 } = await mutate({
|
|
||||||
mutation: CreateGroupRoom,
|
|
||||||
variables: { groupId: 'g2' },
|
|
||||||
})
|
|
||||||
const g2RoomId = roomG2?.CreateGroupRoom.id
|
|
||||||
|
|
||||||
const g2Members = [
|
|
||||||
{ user: bobDerBaumeister, name: 'Bob' },
|
|
||||||
{ user: jennyRostock, name: 'Jenny' },
|
|
||||||
{ user: dewey, name: 'Dewey' },
|
|
||||||
{ user: louie, name: 'Louie' },
|
|
||||||
{ user: dagobert, name: 'Dagobert' },
|
|
||||||
]
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
const member = g2Members[i % g2Members.length]
|
|
||||||
authenticatedUser = await member.user.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: {
|
|
||||||
roomId: g2RoomId,
|
|
||||||
content: faker.lorem.sentence(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group g0 (Investigative Journalism) - intentionally NO chat seeded
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { Integer, Node } from 'neo4j-driver'
|
|||||||
|
|
||||||
export interface CommentDbProperties {
|
export interface CommentDbProperties {
|
||||||
content: string
|
content: string
|
||||||
|
contentExcerpt: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
deleted: boolean
|
deleted: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export interface LocationDbProperties {
|
|||||||
namePL: string
|
namePL: string
|
||||||
namePT: string
|
namePT: string
|
||||||
nameRU: string
|
nameRU: string
|
||||||
nameSQ: string
|
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface MessageDbProperties {
|
|||||||
id: string
|
id: string
|
||||||
indexId: number
|
indexId: number
|
||||||
saved: boolean
|
saved: boolean
|
||||||
|
seen: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message = Node<Integer, MessageDbProperties>
|
export type Message = Node<Integer, MessageDbProperties>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { Integer, Node } from 'neo4j-driver'
|
|||||||
export interface PostDbProperties {
|
export interface PostDbProperties {
|
||||||
clickedCount: number
|
clickedCount: number
|
||||||
content: string
|
content: string
|
||||||
|
contentExcerpt: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
deleted: boolean
|
deleted: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Notificación",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Rol en el grupo cambiado",
|
|
||||||
"chatMessage": "Nuevo mensaje de chat",
|
|
||||||
"commentedOnPost": "Nuevo comentario en una publicación",
|
|
||||||
"followedUserPosted": "Nueva publicación de un usuario seguido",
|
|
||||||
"mentionedInComment": "Mencionado en un comentario",
|
|
||||||
"mentionedInPost": "Mencionado en una publicación",
|
|
||||||
"newEmail": "Nueva dirección de correo electrónico",
|
|
||||||
"removedUserFromGroup": "Eliminado del grupo",
|
|
||||||
"postInGroup": "Nueva publicación en el grupo",
|
|
||||||
"resetPassword": "Restablecer contraseña",
|
|
||||||
"userJoinedGroup": "Un usuario se unió al grupo",
|
|
||||||
"userLeftGroup": "Un usuario abandonó el grupo",
|
|
||||||
"wrongEmail": "¿Correo electrónico incorrecto?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Gracias por registrarte – nos alegra tenerte con nosotros. Solo falta un pequeño paso antes de que podamos cambiar el mundo juntos … Por favor, confirma tu dirección de correo electrónico haciendo clic en el botón de abajo:",
|
|
||||||
"codeHint": "Si el botón de arriba no funciona, también puedes copiar el siguiente código en la ventana de tu navegador: ",
|
|
||||||
"codeHintException": "Sin embargo, esto solo funciona si te has registrado a través de nuestro sitio web.",
|
|
||||||
"notYouStart": "Si no te has registrado en ",
|
|
||||||
"notYouEnd": " te recomendamos que lo visites. Es una red social de personas para personas que quieren conectarse y cambiar el mundo juntas.",
|
|
||||||
"ps": "PD: Si ignoras este correo electrónico, no crearemos una cuenta para ti. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Si el botón de arriba no funciona, también puedes copiar el siguiente código en la ventana de tu navegador: ",
|
|
||||||
"introduction": "¿Quieres cambiar tu dirección de correo electrónico? ¡No hay problema! Simplemente haz clic en el botón de abajo para verificar tu nueva dirección:",
|
|
||||||
"doNotChange": "Si no quieres cambiar tu dirección de correo electrónico, simplemente ignora este mensaje. "
|
|
||||||
},
|
|
||||||
"support": "Si tienes preguntas o problemas, no dudes en contactar con nuestro soporte: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Confirma tu dirección de correo electrónico",
|
|
||||||
"resetPassword": "Restablecer contraseña",
|
|
||||||
"tryAgain": "Probar con otro correo electrónico",
|
|
||||||
"verifyEmail": "Verificar dirección de correo electrónico",
|
|
||||||
"viewChat": "Ver chat",
|
|
||||||
"viewComment": "Ver comentario",
|
|
||||||
"viewGroup": "Ver grupo",
|
|
||||||
"viewPost": "Ver publicación"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Hola",
|
|
||||||
"seeYou": "¡Hasta pronto en ",
|
|
||||||
"yourTeam": "– El equipo de {team}",
|
|
||||||
"settingsHint": "PD: Si no quieres recibir más correos electrónicos, cambia tu ",
|
|
||||||
"settingsName": "configuración de notificaciones",
|
|
||||||
"welcome": "Bienvenido a"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Si el botón de arriba no funciona, también puedes copiar el siguiente código en la ventana de tu navegador: ",
|
|
||||||
"ignore": "Si no has solicitado una nueva contraseña, simplemente ignora este correo electrónico.",
|
|
||||||
"introduction": "¿Has olvidado tu contraseña? ¡No hay problema! Simplemente haz clic en el botón de abajo para restablecerla en las próximas 24 horas:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " o si no querías restablecer tu contraseña, simplemente ignora este correo electrónico.",
|
|
||||||
"ignoreStart": "Si no tienes una cuenta en ",
|
|
||||||
"introduction": "Has solicitado un restablecimiento de contraseña, pero lamentablemente no hemos encontrado ninguna cuenta asociada a tu dirección de correo electrónico. ¿Te registraste quizás con otra dirección?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "tu rol en el grupo «{groupName}» ha sido cambiado. Haz clic en el botón para ver este grupo:",
|
|
||||||
"chatMessageStart": "has recibido un nuevo mensaje de chat de ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " ha comentado en una publicación que sigues con el título «{postTitle}». Haz clic en el botón para ver este comentario:",
|
|
||||||
"followedUserPosted": ", un usuario al que sigues, ha escrito una nueva publicación con el título «{postTitle}». Haz clic en el botón para ver esta publicación:",
|
|
||||||
"mentionedInComment": " te ha mencionado en un comentario de la publicación con el título «{postTitle}». Haz clic en el botón para ver este comentario:",
|
|
||||||
"mentionedInPost": " te ha mencionado en una publicación con el título «{postTitle}». Haz clic en el botón para ver esta publicación:",
|
|
||||||
"removedUserFromGroup": "has sido eliminado del grupo «{groupName}».",
|
|
||||||
"postInGroup": "alguien ha escrito una nueva publicación con el título «{postTitle}» en uno de tus grupos. Haz clic en el botón para ver esta publicación:",
|
|
||||||
"userJoinedGroup": " se ha unido al grupo «{groupName}». Haz clic en el botón para ver este grupo:",
|
|
||||||
"userLeftGroup": " ha abandonado el grupo «{groupName}». Haz clic en el botón para ver este grupo:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Notification",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Rôle dans le groupe modifié",
|
|
||||||
"chatMessage": "Nouveau message de chat",
|
|
||||||
"commentedOnPost": "Nouveau commentaire sur une publication",
|
|
||||||
"followedUserPosted": "Nouvelle publication d'un utilisateur suivi",
|
|
||||||
"mentionedInComment": "Mentionné dans un commentaire",
|
|
||||||
"mentionedInPost": "Mentionné dans une publication",
|
|
||||||
"newEmail": "Nouvelle adresse e-mail",
|
|
||||||
"removedUserFromGroup": "Retiré du groupe",
|
|
||||||
"postInGroup": "Nouvelle publication dans le groupe",
|
|
||||||
"resetPassword": "Réinitialiser le mot de passe",
|
|
||||||
"userJoinedGroup": "Un utilisateur a rejoint le groupe",
|
|
||||||
"userLeftGroup": "Un utilisateur a quitté le groupe",
|
|
||||||
"wrongEmail": "Mauvaise adresse e-mail ?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Merci de nous avoir rejoints – nous sommes ravis de vous compter parmi nous. Il ne reste plus qu'une petite étape avant de pouvoir changer le monde ensemble … Veuillez confirmer votre adresse e-mail en cliquant sur le bouton ci-dessous :",
|
|
||||||
"codeHint": "Si le bouton ci-dessus ne fonctionne pas, vous pouvez aussi copier le code suivant dans votre navigateur : ",
|
|
||||||
"codeHintException": "Cependant, cela ne fonctionne que si vous vous êtes inscrit via notre site web.",
|
|
||||||
"notYouStart": "Si vous ne vous êtes pas inscrit sur ",
|
|
||||||
"notYouEnd": " nous vous recommandons d'y jeter un œil ! C'est un réseau social de personnes pour des personnes qui veulent se connecter et changer le monde ensemble.",
|
|
||||||
"ps": "PS : Si vous ignorez cet e-mail, nous ne créerons pas de compte pour vous. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Si le bouton ci-dessus ne fonctionne pas, vous pouvez aussi copier le code suivant dans votre navigateur : ",
|
|
||||||
"introduction": "Vous souhaitez changer votre adresse e-mail ? Pas de problème ! Cliquez simplement sur le bouton ci-dessous pour vérifier votre nouvelle adresse :",
|
|
||||||
"doNotChange": "Si vous ne souhaitez pas changer votre adresse e-mail, n'hésitez pas à ignorer ce message. "
|
|
||||||
},
|
|
||||||
"support": "Si vous avez des questions ou des problèmes, n'hésitez pas à contacter notre support : ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Confirmer votre adresse e-mail",
|
|
||||||
"resetPassword": "Réinitialiser le mot de passe",
|
|
||||||
"tryAgain": "Essayer une autre adresse e-mail",
|
|
||||||
"verifyEmail": "Vérifier l'adresse e-mail",
|
|
||||||
"viewChat": "Voir le chat",
|
|
||||||
"viewComment": "Voir le commentaire",
|
|
||||||
"viewGroup": "Voir le groupe",
|
|
||||||
"viewPost": "Voir la publication"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Bonjour",
|
|
||||||
"seeYou": "À bientôt sur ",
|
|
||||||
"yourTeam": "– L'équipe {team}",
|
|
||||||
"settingsHint": "PS : Si vous ne souhaitez plus recevoir d'e-mails, modifiez vos ",
|
|
||||||
"settingsName": "paramètres de notification",
|
|
||||||
"welcome": "Bienvenue sur"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Si le bouton ci-dessus ne fonctionne pas, vous pouvez aussi copier le code suivant dans votre navigateur : ",
|
|
||||||
"ignore": "Si vous n'avez pas demandé de nouveau mot de passe, n'hésitez pas à ignorer cet e-mail.",
|
|
||||||
"introduction": "Vous avez oublié votre mot de passe ? Pas de problème ! Cliquez simplement sur le bouton ci-dessous pour le réinitialiser dans les 24 prochaines heures :"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " ou si vous ne souhaitiez pas réinitialiser votre mot de passe, veuillez ignorer cet e-mail.",
|
|
||||||
"ignoreStart": "Si vous n'avez pas de compte sur ",
|
|
||||||
"introduction": "Vous avez demandé une réinitialisation de mot de passe, mais malheureusement nous n'avons trouvé aucun compte associé à votre adresse e-mail. Vous êtes-vous peut-être inscrit avec une autre adresse ?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "votre rôle dans le groupe « {groupName} » a été modifié. Cliquez sur le bouton pour voir ce groupe :",
|
|
||||||
"chatMessageStart": "vous avez reçu un nouveau message de chat de ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " a commenté une publication que vous suivez avec le titre « {postTitle} ». Cliquez sur le bouton pour voir ce commentaire :",
|
|
||||||
"followedUserPosted": ", un utilisateur que vous suivez, a écrit une nouvelle publication avec le titre « {postTitle} ». Cliquez sur le bouton pour voir cette publication :",
|
|
||||||
"mentionedInComment": " vous a mentionné dans un commentaire sur la publication avec le titre « {postTitle} ». Cliquez sur le bouton pour voir ce commentaire :",
|
|
||||||
"mentionedInPost": " vous a mentionné dans une publication avec le titre « {postTitle} ». Cliquez sur le bouton pour voir cette publication :",
|
|
||||||
"removedUserFromGroup": "vous avez été retiré du groupe « {groupName} ».",
|
|
||||||
"postInGroup": "quelqu'un a écrit une nouvelle publication avec le titre « {postTitle} » dans l'un de vos groupes. Cliquez sur le bouton pour voir cette publication :",
|
|
||||||
"userJoinedGroup": " a rejoint le groupe « {groupName} ». Cliquez sur le bouton pour voir ce groupe :",
|
|
||||||
"userLeftGroup": " a quitté le groupe « {groupName} ». Cliquez sur le bouton pour voir ce groupe :"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Notifica",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Ruolo nel gruppo modificato",
|
|
||||||
"chatMessage": "Nuovo messaggio in chat",
|
|
||||||
"commentedOnPost": "Nuovo commento su un post",
|
|
||||||
"followedUserPosted": "Nuovo post di un utente seguito",
|
|
||||||
"mentionedInComment": "Menzionato in un commento",
|
|
||||||
"mentionedInPost": "Menzionato in un post",
|
|
||||||
"newEmail": "Nuovo indirizzo e-mail",
|
|
||||||
"removedUserFromGroup": "Rimosso dal gruppo",
|
|
||||||
"postInGroup": "Nuovo post nel gruppo",
|
|
||||||
"resetPassword": "Reimposta la password",
|
|
||||||
"userJoinedGroup": "Un utente si è unito al gruppo",
|
|
||||||
"userLeftGroup": "Un utente ha lasciato il gruppo",
|
|
||||||
"wrongEmail": "E-mail sbagliata?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Grazie per esserti registrato – siamo felici di averti con noi. Manca solo un piccolo passo prima di poter cambiare il mondo insieme … Per favore, conferma il tuo indirizzo e-mail cliccando sul pulsante qui sotto:",
|
|
||||||
"codeHint": "Se il pulsante qui sopra non funziona, puoi anche copiare il seguente codice nella finestra del tuo browser: ",
|
|
||||||
"codeHintException": "Tuttavia, questo funziona solo se ti sei registrato tramite il nostro sito web.",
|
|
||||||
"notYouStart": "Se non ti sei registrato su ",
|
|
||||||
"notYouEnd": " ti consigliamo di dargli un'occhiata! È un social network di persone per persone che vogliono connettersi e cambiare il mondo insieme.",
|
|
||||||
"ps": "PS: Se ignori questa e-mail, non creeremo un account per te. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Se il pulsante qui sopra non funziona, puoi anche copiare il seguente codice nella finestra del tuo browser: ",
|
|
||||||
"introduction": "Vuoi cambiare il tuo indirizzo e-mail? Nessun problema! Clicca semplicemente sul pulsante qui sotto per verificare il tuo nuovo indirizzo:",
|
|
||||||
"doNotChange": "Se non vuoi cambiare il tuo indirizzo e-mail, puoi semplicemente ignorare questo messaggio. "
|
|
||||||
},
|
|
||||||
"support": "Se hai domande o problemi, non esitare a contattare il nostro supporto: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Conferma il tuo indirizzo e-mail",
|
|
||||||
"resetPassword": "Reimposta la password",
|
|
||||||
"tryAgain": "Prova con un altro indirizzo e-mail",
|
|
||||||
"verifyEmail": "Verifica l'indirizzo e-mail",
|
|
||||||
"viewChat": "Visualizza la chat",
|
|
||||||
"viewComment": "Visualizza il commento",
|
|
||||||
"viewGroup": "Visualizza il gruppo",
|
|
||||||
"viewPost": "Visualizza il post"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Ciao",
|
|
||||||
"seeYou": "A presto su ",
|
|
||||||
"yourTeam": "– Il team {team}",
|
|
||||||
"settingsHint": "PS: Se non vuoi più ricevere e-mail, modifica le tue ",
|
|
||||||
"settingsName": "impostazioni di notifica",
|
|
||||||
"welcome": "Benvenuto su"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Se il pulsante qui sopra non funziona, puoi anche copiare il seguente codice nella finestra del tuo browser: ",
|
|
||||||
"ignore": "Se non hai richiesto una nuova password, puoi semplicemente ignorare questa e-mail.",
|
|
||||||
"introduction": "Hai dimenticato la tua password? Nessun problema! Clicca semplicemente sul pulsante qui sotto per reimpostarla entro le prossime 24 ore:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " o se non volevi reimpostare la tua password, puoi semplicemente ignorare questa e-mail.",
|
|
||||||
"ignoreStart": "Se non hai un account su ",
|
|
||||||
"introduction": "Hai richiesto un ripristino della password, ma purtroppo non abbiamo trovato un account associato al tuo indirizzo e-mail. Ti sei forse registrato con un altro indirizzo?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "il tuo ruolo nel gruppo \u201e{groupName}\u201c è stato modificato. Clicca sul pulsante per visualizzare questo gruppo:",
|
|
||||||
"chatMessageStart": "hai ricevuto un nuovo messaggio in chat da ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " ha commentato un post che stai seguendo con il titolo \u201e{postTitle}\u201c. Clicca sul pulsante per visualizzare questo commento:",
|
|
||||||
"followedUserPosted": ", un utente che segui, ha scritto un nuovo post con il titolo \u201e{postTitle}\u201c. Clicca sul pulsante per visualizzare questo post:",
|
|
||||||
"mentionedInComment": " ti ha menzionato in un commento al post con il titolo \u201e{postTitle}\u201c. Clicca sul pulsante per visualizzare questo commento:",
|
|
||||||
"mentionedInPost": " ti ha menzionato in un post con il titolo \u201e{postTitle}\u201c. Clicca sul pulsante per visualizzare questo post:",
|
|
||||||
"removedUserFromGroup": "sei stato rimosso dal gruppo \u201e{groupName}\u201c.",
|
|
||||||
"postInGroup": "qualcuno ha scritto un nuovo post con il titolo \u201e{postTitle}\u201c in uno dei tuoi gruppi. Clicca sul pulsante per visualizzare questo post:",
|
|
||||||
"userJoinedGroup": " si è unito al gruppo \u201e{groupName}\u201c. Clicca sul pulsante per visualizzare questo gruppo:",
|
|
||||||
"userLeftGroup": " ha lasciato il gruppo \u201e{groupName}\u201c. Clicca sul pulsante per visualizzare questo gruppo:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Melding",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Rol in groep gewijzigd",
|
|
||||||
"chatMessage": "Nieuw chatbericht",
|
|
||||||
"commentedOnPost": "Nieuwe reactie op bericht",
|
|
||||||
"followedUserPosted": "Nieuw bericht van gevolgde gebruiker",
|
|
||||||
"mentionedInComment": "Vermeld in reactie",
|
|
||||||
"mentionedInPost": "Vermeld in bericht",
|
|
||||||
"newEmail": "Nieuw e-mailadres",
|
|
||||||
"removedUserFromGroup": "Uit groep verwijderd",
|
|
||||||
"postInGroup": "Nieuw bericht in groep",
|
|
||||||
"resetPassword": "Wachtwoord herstellen",
|
|
||||||
"userJoinedGroup": "Gebruiker is lid geworden van groep",
|
|
||||||
"userLeftGroup": "Gebruiker heeft groep verlaten",
|
|
||||||
"wrongEmail": "Verkeerd e-mailadres?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Bedankt voor je aanmelding – fijn dat je erbij bent. Er is nog maar één klein stapje nodig voordat we samen de wereld kunnen verbeteren … Bevestig je e-mailadres door op de onderstaande knop te klikken:",
|
|
||||||
"codeHint": "Als de bovenstaande knop niet werkt, kun je ook de volgende code in je browservenster kopiëren: ",
|
|
||||||
"codeHintException": "Dit werkt echter alleen als je je via onze website hebt geregistreerd.",
|
|
||||||
"notYouStart": "Als je je niet hebt aangemeld bij ",
|
|
||||||
"notYouEnd": " raden we je aan om een kijkje te nemen! Het is een sociaal netwerk van mensen voor mensen die samen de wereld willen veranderen.",
|
|
||||||
"ps": "PS: Als je deze e-mail negeert, maken we geen account voor je aan. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Als de bovenstaande knop niet werkt, kun je ook de volgende code in je browservenster kopiëren: ",
|
|
||||||
"introduction": "Wil je je e-mailadres wijzigen? Geen probleem! Klik op de onderstaande knop om je nieuwe adres te verifiëren:",
|
|
||||||
"doNotChange": "Als je je e-mailadres niet wilt wijzigen, kun je dit bericht gewoon negeren. "
|
|
||||||
},
|
|
||||||
"support": "Als je vragen of problemen hebt, neem dan gerust contact op met onze ondersteuning: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Bevestig je e-mailadres",
|
|
||||||
"resetPassword": "Wachtwoord herstellen",
|
|
||||||
"tryAgain": "Probeer een ander e-mailadres",
|
|
||||||
"verifyEmail": "E-mailadres verifiëren",
|
|
||||||
"viewChat": "Chat bekijken",
|
|
||||||
"viewComment": "Reactie bekijken",
|
|
||||||
"viewGroup": "Groep bekijken",
|
|
||||||
"viewPost": "Bericht bekijken"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Hallo",
|
|
||||||
"seeYou": "Tot snel bij ",
|
|
||||||
"yourTeam": "– Het {team} Team",
|
|
||||||
"settingsHint": "PS: Als je geen e-mails meer wilt ontvangen, wijzig dan je ",
|
|
||||||
"settingsName": "meldingsinstellingen",
|
|
||||||
"welcome": "Welkom bij"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Als de bovenstaande knop niet werkt, kun je ook de volgende code in je browservenster kopiëren: ",
|
|
||||||
"ignore": "Als je geen nieuw wachtwoord hebt aangevraagd, kun je deze e-mail gewoon negeren.",
|
|
||||||
"introduction": "Wachtwoord vergeten? Geen probleem! Klik op de onderstaande knop om het binnen 24 uur te herstellen:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " hebt of als je je wachtwoord niet wilde herstellen, kun je deze e-mail gewoon negeren.",
|
|
||||||
"ignoreStart": "Als je geen account hebt bij ",
|
|
||||||
"introduction": "Je hebt een wachtwoordherstel aangevraagd, maar we konden helaas geen account vinden dat aan je e-mailadres is gekoppeld. Heb je je misschien met een ander adres aangemeld?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "je rol in de groep \u201e{groupName}\u201c is gewijzigd. Klik op de knop om deze groep te bekijken:",
|
|
||||||
"chatMessageStart": "je hebt een nieuw chatbericht ontvangen van ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " heeft gereageerd op een bericht dat je volgt met de titel \u201e{postTitle}\u201c. Klik op de knop om deze reactie te bekijken:",
|
|
||||||
"followedUserPosted": ", een gebruiker die je volgt, heeft een nieuw bericht geschreven met de titel \u201e{postTitle}\u201c. Klik op de knop om dit bericht te bekijken:",
|
|
||||||
"mentionedInComment": " heeft je vermeld in een reactie op het bericht met de titel \u201e{postTitle}\u201c. Klik op de knop om deze reactie te bekijken:",
|
|
||||||
"mentionedInPost": " heeft je vermeld in een bericht met de titel \u201e{postTitle}\u201c. Klik op de knop om dit bericht te bekijken:",
|
|
||||||
"removedUserFromGroup": "je bent verwijderd uit de groep \u201e{groupName}\u201c.",
|
|
||||||
"postInGroup": "iemand heeft een nieuw bericht geschreven met de titel \u201e{postTitle}\u201c in een van je groepen. Klik op de knop om dit bericht te bekijken:",
|
|
||||||
"userJoinedGroup": " is lid geworden van de groep \u201e{groupName}\u201c. Klik op de knop om deze groep te bekijken:",
|
|
||||||
"userLeftGroup": " heeft de groep \u201e{groupName}\u201c verlaten. Klik op de knop om deze groep te bekijken:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Powiadomienie",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Zmieniono rolę w grupie",
|
|
||||||
"chatMessage": "Nowa wiadomość na czacie",
|
|
||||||
"commentedOnPost": "Nowy komentarz do wpisu",
|
|
||||||
"followedUserPosted": "Nowy wpis obserwowanego użytkownika",
|
|
||||||
"mentionedInComment": "Wspomniano w komentarzu",
|
|
||||||
"mentionedInPost": "Wspomniano we wpisie",
|
|
||||||
"newEmail": "Nowy adres e-mail",
|
|
||||||
"removedUserFromGroup": "Usunięto z grupy",
|
|
||||||
"postInGroup": "Nowy wpis w grupie",
|
|
||||||
"resetPassword": "Zresetuj hasło",
|
|
||||||
"userJoinedGroup": "Użytkownik dołączył do grupy",
|
|
||||||
"userLeftGroup": "Użytkownik opuścił grupę",
|
|
||||||
"wrongEmail": "Nieprawidłowy e-mail?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Dziękujemy za rejestrację – cieszymy się, że jesteś z nami. Został jeszcze tylko jeden mały krok, zanim razem zaczniemy zmieniać świat … Potwierdź swój adres e-mail, klikając poniższy przycisk:",
|
|
||||||
"codeHint": "Jeśli powyższy przycisk nie działa, możesz też skopiować następujący kod do okna przeglądarki: ",
|
|
||||||
"codeHintException": "Działa to jednak tylko wtedy, gdy zarejestrowałeś się przez naszą stronę internetową.",
|
|
||||||
"notYouStart": "Jeśli nie rejestrowałeś się w ",
|
|
||||||
"notYouEnd": " polecamy zajrzeć! To sieć społecznościowa ludzi dla ludzi, którzy chcą się łączyć i wspólnie zmieniać świat.",
|
|
||||||
"ps": "PS: Jeśli zignorujesz tego e-maila, nie utworzymy dla Ciebie konta. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Jeśli powyższy przycisk nie działa, możesz też skopiować następujący kod do okna przeglądarki: ",
|
|
||||||
"introduction": "Chcesz zmienić swój adres e-mail? Żaden problem! Po prostu kliknij poniższy przycisk, aby zweryfikować nowy adres:",
|
|
||||||
"doNotChange": "Jeśli nie chcesz zmieniać swojego adresu e-mail, po prostu zignoruj tę wiadomość. "
|
|
||||||
},
|
|
||||||
"support": "Jeśli masz pytania lub problemy, skontaktuj się z naszym wsparciem: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Potwierdź swój adres e-mail",
|
|
||||||
"resetPassword": "Zresetuj hasło",
|
|
||||||
"tryAgain": "Spróbuj z innym adresem e-mail",
|
|
||||||
"verifyEmail": "Zweryfikuj adres e-mail",
|
|
||||||
"viewChat": "Zobacz czat",
|
|
||||||
"viewComment": "Zobacz komentarz",
|
|
||||||
"viewGroup": "Zobacz grupę",
|
|
||||||
"viewPost": "Zobacz wpis"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Cześć",
|
|
||||||
"seeYou": "Do zobaczenia wkrótce na ",
|
|
||||||
"yourTeam": "– Zespół {team}",
|
|
||||||
"settingsHint": "PS: Jeśli nie chcesz już otrzymywać e-maili, zmień swoje ",
|
|
||||||
"settingsName": "ustawienia powiadomień",
|
|
||||||
"welcome": "Witamy w"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Jeśli powyższy przycisk nie działa, możesz też skopiować następujący kod do okna przeglądarki: ",
|
|
||||||
"ignore": "Jeśli nie prosiłeś o nowe hasło, po prostu zignoruj tego e-maila.",
|
|
||||||
"introduction": "Zapomniałeś hasła? Żaden problem! Po prostu kliknij poniższy przycisk, aby je zresetować w ciągu najbliższych 24 godzin:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " lub jeśli nie chciałeś resetować hasła, po prostu zignoruj tego e-maila.",
|
|
||||||
"ignoreStart": "Jeśli nie masz konta w ",
|
|
||||||
"introduction": "Poprosiłeś o zresetowanie hasła, ale niestety nie znaleźliśmy konta powiązanego z Twoim adresem e-mail. Czy może zarejestrowałeś się z innym adresem?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "Twoja rola w grupie „{groupName}“ została zmieniona. Kliknij przycisk, aby zobaczyć tę grupę:",
|
|
||||||
"chatMessageStart": "otrzymałeś nową wiadomość na czacie od ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " skomentował wpis, który obserwujesz, o tytule „{postTitle}“. Kliknij przycisk, aby zobaczyć ten komentarz:",
|
|
||||||
"followedUserPosted": ", użytkownik, którego obserwujesz, napisał nowy wpis o tytule „{postTitle}“. Kliknij przycisk, aby zobaczyć ten wpis:",
|
|
||||||
"mentionedInComment": " wspomniał o Tobie w komentarzu do wpisu o tytule „{postTitle}“. Kliknij przycisk, aby zobaczyć ten komentarz:",
|
|
||||||
"mentionedInPost": " wspomniał o Tobie we wpisie o tytule „{postTitle}“. Kliknij przycisk, aby zobaczyć ten wpis:",
|
|
||||||
"removedUserFromGroup": "zostałeś usunięty z grupy „{groupName}“.",
|
|
||||||
"postInGroup": "ktoś napisał nowy wpis o tytule „{postTitle}“ w jednej z Twoich grup. Kliknij przycisk, aby zobaczyć ten wpis:",
|
|
||||||
"userJoinedGroup": " dołączył do grupy „{groupName}“. Kliknij przycisk, aby zobaczyć tę grupę:",
|
|
||||||
"userLeftGroup": " opuścił grupę „{groupName}“. Kliknij przycisk, aby zobaczyć tę grupę:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Notificação",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Função no grupo alterada",
|
|
||||||
"chatMessage": "Nova mensagem no chat",
|
|
||||||
"commentedOnPost": "Novo comentário numa publicação",
|
|
||||||
"followedUserPosted": "Nova publicação de um utilizador seguido",
|
|
||||||
"mentionedInComment": "Mencionado num comentário",
|
|
||||||
"mentionedInPost": "Mencionado numa publicação",
|
|
||||||
"newEmail": "Novo endereço de e-mail",
|
|
||||||
"removedUserFromGroup": "Removido do grupo",
|
|
||||||
"postInGroup": "Nova publicação no grupo",
|
|
||||||
"resetPassword": "Redefinir palavra-passe",
|
|
||||||
"userJoinedGroup": "Um utilizador juntou-se ao grupo",
|
|
||||||
"userLeftGroup": "Um utilizador saiu do grupo",
|
|
||||||
"wrongEmail": "E-mail errado?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Obrigado por te registares – é ótimo ter-te connosco. Falta apenas um pequeno passo antes de podermos mudar o mundo juntos … Por favor, confirma o teu endereço de e-mail clicando no botão abaixo:",
|
|
||||||
"codeHint": "Se o botão acima não funcionar, também podes copiar o seguinte código na janela do teu navegador: ",
|
|
||||||
"codeHintException": "No entanto, isto só funciona se te registaste através do nosso site.",
|
|
||||||
"notYouStart": "Se não te registaste em ",
|
|
||||||
"notYouEnd": " recomendamos que dês uma olhada! É uma rede social de pessoas para pessoas que querem conectar-se e mudar o mundo juntas.",
|
|
||||||
"ps": "PS: Se ignorares este e-mail, não criaremos uma conta para ti. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Se o botão acima não funcionar, também podes copiar o seguinte código na janela do teu navegador: ",
|
|
||||||
"introduction": "Queres alterar o teu endereço de e-mail? Sem problema! Basta clicar no botão abaixo para verificar o teu novo endereço:",
|
|
||||||
"doNotChange": "Se não quiseres alterar o teu endereço de e-mail, podes simplesmente ignorar esta mensagem. "
|
|
||||||
},
|
|
||||||
"support": "Se tiveres dúvidas ou problemas, não hesites em contactar o nosso suporte: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Confirma o teu endereço de e-mail",
|
|
||||||
"resetPassword": "Redefinir palavra-passe",
|
|
||||||
"tryAgain": "Tentar outro endereço de e-mail",
|
|
||||||
"verifyEmail": "Verificar endereço de e-mail",
|
|
||||||
"viewChat": "Ver chat",
|
|
||||||
"viewComment": "Ver comentário",
|
|
||||||
"viewGroup": "Ver grupo",
|
|
||||||
"viewPost": "Ver publicação"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Olá",
|
|
||||||
"seeYou": "Até breve em ",
|
|
||||||
"yourTeam": "– A equipa {team}",
|
|
||||||
"settingsHint": "PS: Se não quiseres receber mais e-mails, altera as tuas ",
|
|
||||||
"settingsName": "definições de notificação",
|
|
||||||
"welcome": "Bem-vindo a"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Se o botão acima não funcionar, também podes copiar o seguinte código na janela do teu navegador: ",
|
|
||||||
"ignore": "Se não solicitaste uma nova palavra-passe, podes simplesmente ignorar este e-mail.",
|
|
||||||
"introduction": "Esqueceste a tua palavra-passe? Sem problema! Basta clicar no botão abaixo para a redefinir nas próximas 24 horas:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " ou se não querias redefinir a tua palavra-passe, podes simplesmente ignorar este e-mail.",
|
|
||||||
"ignoreStart": "Se não tens uma conta em ",
|
|
||||||
"introduction": "Solicitaste uma redefinição de palavra-passe, mas infelizmente não encontrámos nenhuma conta associada ao teu endereço de e-mail. Será que te registaste com outro endereço?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "a tua função no grupo \u201e{groupName}\u201c foi alterada. Clica no botão para ver este grupo:",
|
|
||||||
"chatMessageStart": "recebeste uma nova mensagem no chat de ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " comentou numa publicação que estás a seguir com o título \u201e{postTitle}\u201c. Clica no botão para ver este comentário:",
|
|
||||||
"followedUserPosted": ", um utilizador que segues, escreveu uma nova publicação com o título \u201e{postTitle}\u201c. Clica no botão para ver esta publicação:",
|
|
||||||
"mentionedInComment": " mencionou-te num comentário na publicação com o título \u201e{postTitle}\u201c. Clica no botão para ver este comentário:",
|
|
||||||
"mentionedInPost": " mencionou-te numa publicação com o título \u201e{postTitle}\u201c. Clica no botão para ver esta publicação:",
|
|
||||||
"removedUserFromGroup": "foste removido do grupo \u201e{groupName}\u201c.",
|
|
||||||
"postInGroup": "alguém escreveu uma nova publicação com o título \u201e{postTitle}\u201c num dos teus grupos. Clica no botão para ver esta publicação:",
|
|
||||||
"userJoinedGroup": " juntou-se ao grupo \u201e{groupName}\u201c. Clica no botão para ver este grupo:",
|
|
||||||
"userLeftGroup": " saiu do grupo \u201e{groupName}\u201c. Clica no botão para ver este grupo:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Уведомление",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Роль в группе изменена",
|
|
||||||
"chatMessage": "Новое сообщение в чате",
|
|
||||||
"commentedOnPost": "Новый комментарий к публикации",
|
|
||||||
"followedUserPosted": "Новая публикация от пользователя, на которого вы подписаны",
|
|
||||||
"mentionedInComment": "Упоминание в комментарии",
|
|
||||||
"mentionedInPost": "Упоминание в публикации",
|
|
||||||
"newEmail": "Новый адрес электронной почты",
|
|
||||||
"removedUserFromGroup": "Удалён из группы",
|
|
||||||
"postInGroup": "Новая публикация в группе",
|
|
||||||
"resetPassword": "Сброс пароля",
|
|
||||||
"userJoinedGroup": "Пользователь присоединился к группе",
|
|
||||||
"userLeftGroup": "Пользователь покинул группу",
|
|
||||||
"wrongEmail": "Неверный адрес электронной почты?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Спасибо за регистрацию – мы рады, что вы с нами. Остался всего один маленький шаг, прежде чем мы сможем вместе менять мир … Пожалуйста, подтвердите свой адрес электронной почты, нажав на кнопку ниже:",
|
|
||||||
"codeHint": "Если кнопка выше не работает, вы также можете скопировать следующий код в окно браузера: ",
|
|
||||||
"codeHintException": "Однако это работает только в том случае, если вы зарегистрировались через наш сайт.",
|
|
||||||
"notYouStart": "Если вы не регистрировались на ",
|
|
||||||
"notYouEnd": " рекомендуем заглянуть! Это социальная сеть от людей для людей, которые хотят объединяться и вместе менять мир.",
|
|
||||||
"ps": "PS: Если вы проигнорируете это письмо, мы не создадим для вас аккаунт. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Если кнопка выше не работает, вы также можете скопировать следующий код в окно браузера: ",
|
|
||||||
"introduction": "Хотите изменить свой адрес электронной почты? Нет проблем! Просто нажмите на кнопку ниже, чтобы подтвердить новый адрес:",
|
|
||||||
"doNotChange": "Если вы не хотите менять свой адрес электронной почты, просто проигнорируйте это сообщение. "
|
|
||||||
},
|
|
||||||
"support": "Если у вас есть вопросы или проблемы, обращайтесь в нашу службу поддержки: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Подтвердите свой адрес электронной почты",
|
|
||||||
"resetPassword": "Сбросить пароль",
|
|
||||||
"tryAgain": "Попробовать другой адрес электронной почты",
|
|
||||||
"verifyEmail": "Подтвердить адрес электронной почты",
|
|
||||||
"viewChat": "Открыть чат",
|
|
||||||
"viewComment": "Посмотреть комментарий",
|
|
||||||
"viewGroup": "Посмотреть группу",
|
|
||||||
"viewPost": "Посмотреть публикацию"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Здравствуйте",
|
|
||||||
"seeYou": "До скорой встречи на ",
|
|
||||||
"yourTeam": "– Команда {team}",
|
|
||||||
"settingsHint": "PS: Если вы больше не хотите получать электронные письма, измените свои ",
|
|
||||||
"settingsName": "настройки уведомлений",
|
|
||||||
"welcome": "Добро пожаловать в"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Если кнопка выше не работает, вы также можете скопировать следующий код в окно браузера: ",
|
|
||||||
"ignore": "Если вы не запрашивали новый пароль, просто проигнорируйте это письмо.",
|
|
||||||
"introduction": "Забыли пароль? Нет проблем! Просто нажмите на кнопку ниже, чтобы сбросить его в течение ближайших 24 часов:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " или если вы не хотели сбрасывать пароль, просто проигнорируйте это письмо.",
|
|
||||||
"ignoreStart": "Если у вас нет аккаунта на ",
|
|
||||||
"introduction": "Вы запросили сброс пароля, но, к сожалению, мы не нашли аккаунт, связанный с вашим адресом электронной почты. Возможно, вы зарегистрировались с другим адресом?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "ваша роль в группе «{groupName}» была изменена. Нажмите на кнопку, чтобы посмотреть эту группу:",
|
|
||||||
"chatMessageStart": "вы получили новое сообщение в чате от ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " прокомментировал публикацию, за которой вы следите, с заголовком «{postTitle}». Нажмите на кнопку, чтобы посмотреть этот комментарий:",
|
|
||||||
"followedUserPosted": ", пользователь, на которого вы подписаны, написал новую публикацию с заголовком «{postTitle}». Нажмите на кнопку, чтобы посмотреть эту публикацию:",
|
|
||||||
"mentionedInComment": " упомянул вас в комментарии к публикации с заголовком «{postTitle}». Нажмите на кнопку, чтобы посмотреть этот комментарий:",
|
|
||||||
"mentionedInPost": " упомянул вас в публикации с заголовком «{postTitle}». Нажмите на кнопку, чтобы посмотреть эту публикацию:",
|
|
||||||
"removedUserFromGroup": "вы были удалены из группы «{groupName}».",
|
|
||||||
"postInGroup": "кто-то написал новую публикацию с заголовком «{postTitle}» в одной из ваших групп. Нажмите на кнопку, чтобы посмотреть эту публикацию:",
|
|
||||||
"userJoinedGroup": " присоединился к группе «{groupName}». Нажмите на кнопку, чтобы посмотреть эту группу:",
|
|
||||||
"userLeftGroup": " покинул группу «{groupName}». Нажмите на кнопку, чтобы посмотреть эту группу:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Njoftim",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Roli në grup u ndryshua",
|
|
||||||
"chatMessage": "Mesazh i ri në chat",
|
|
||||||
"commentedOnPost": "Koment i ri në postim",
|
|
||||||
"followedUserPosted": "Postim i ri nga një përdorues i ndjekur",
|
|
||||||
"mentionedInComment": "U përmend në koment",
|
|
||||||
"mentionedInPost": "U përmend në postim",
|
|
||||||
"newEmail": "Adresë e re e-mail",
|
|
||||||
"removedUserFromGroup": "U hoq nga grupi",
|
|
||||||
"postInGroup": "Postim i ri në grup",
|
|
||||||
"resetPassword": "Rivendos fjalëkalimin",
|
|
||||||
"userJoinedGroup": "Një përdorues u bashkua me grupin",
|
|
||||||
"userLeftGroup": "Një përdorues u largua nga grupi",
|
|
||||||
"wrongEmail": "E-mail e gabuar?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Faleminderit që u regjistruat – jemi të lumtur që jeni me ne. Mbetet vetëm një hap i vogël përpara se të ndryshojmë botën së bashku … Ju lutemi konfirmoni adresën tuaj të e-mailit duke klikuar butonin më poshtë:",
|
|
||||||
"codeHint": "Nëse butoni më sipër nuk funksionon, mund të kopjoni kodin e mëposhtëm në dritaren e shfletuesit tuaj: ",
|
|
||||||
"codeHintException": "Megjithatë, kjo funksionon vetëm nëse jeni regjistruar përmes faqes sonë të internetit.",
|
|
||||||
"notYouStart": "Nëse nuk jeni regjistruar në ",
|
|
||||||
"notYouEnd": " ju rekomandojmë ta shikoni! Është një rrjet social nga njerëz për njerëz që duan të lidhen dhe të ndryshojnë botën së bashku.",
|
|
||||||
"ps": "PS: Nëse e injoroni këtë e-mail, nuk do të krijojmë një llogari për ju. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Nëse butoni më sipër nuk funksionon, mund të kopjoni kodin e mëposhtëm në dritaren e shfletuesit tuaj: ",
|
|
||||||
"introduction": "Dëshironi të ndryshoni adresën tuaj të e-mailit? Asnjë problem! Thjesht klikoni butonin më poshtë për të verifikuar adresën tuaj të re:",
|
|
||||||
"doNotChange": "Nëse nuk dëshironi të ndryshoni adresën tuaj të e-mailit, thjesht injoroni këtë mesazh. "
|
|
||||||
},
|
|
||||||
"support": "Nëse keni pyetje ose probleme, mos hezitoni të kontaktoni mbështetjen tonë: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Konfirmoni adresën tuaj të e-mailit",
|
|
||||||
"resetPassword": "Rivendos fjalëkalimin",
|
|
||||||
"tryAgain": "Provoni me një e-mail tjetër",
|
|
||||||
"verifyEmail": "Verifiko adresën e e-mailit",
|
|
||||||
"viewChat": "Shiko chatin",
|
|
||||||
"viewComment": "Shiko komentin",
|
|
||||||
"viewGroup": "Shiko grupin",
|
|
||||||
"viewPost": "Shiko postimin"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Përshëndetje",
|
|
||||||
"seeYou": "Shihemi së shpejti në ",
|
|
||||||
"yourTeam": "– Ekipi {team}",
|
|
||||||
"settingsHint": "PS: Nëse nuk dëshironi të merrni më e-mail, ndryshoni ",
|
|
||||||
"settingsName": "cilësimet e njoftimeve",
|
|
||||||
"welcome": "Mirë se vini në"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Nëse butoni më sipër nuk funksionon, mund të kopjoni kodin e mëposhtëm në dritaren e shfletuesit tuaj: ",
|
|
||||||
"ignore": "Nëse nuk keni kërkuar një fjalëkalim të ri, thjesht injoroni këtë e-mail.",
|
|
||||||
"introduction": "Keni harruar fjalëkalimin? Asnjë problem! Thjesht klikoni butonin më poshtë për ta rivendosur brenda 24 orëve të ardhshme:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " ose nëse nuk dëshironit të rivendosnit fjalëkalimin, ju lutemi injoroni këtë e-mail.",
|
|
||||||
"ignoreStart": "Nëse nuk keni një llogari në ",
|
|
||||||
"introduction": "Ju kërkuat një rivendosje të fjalëkalimit, por fatkeqësisht nuk gjetëm asnjë llogari të lidhur me adresën tuaj të e-mailit. A mund të jeni regjistruar me një adresë tjetër?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "roli juaj në grupin \u201e{groupName}\u201c u ndryshua. Klikoni butonin për të parë këtë grup:",
|
|
||||||
"chatMessageStart": "keni marrë një mesazh të ri në chat nga ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " komentoi në një postim që po ndiqni me titullin \u201e{postTitle}\u201c. Klikoni butonin për të parë këtë koment:",
|
|
||||||
"followedUserPosted": ", një përdorues që ndiqni, shkroi një postim të ri me titullin \u201e{postTitle}\u201c. Klikoni butonin për të parë këtë postim:",
|
|
||||||
"mentionedInComment": " ju përmendi në një koment te postimi me titullin \u201e{postTitle}\u201c. Klikoni butonin për të parë këtë koment:",
|
|
||||||
"mentionedInPost": " ju përmendi në një postim me titullin \u201e{postTitle}\u201c. Klikoni butonin për të parë këtë postim:",
|
|
||||||
"removedUserFromGroup": "jeni hequr nga grupi \u201e{groupName}\u201c.",
|
|
||||||
"postInGroup": "dikush shkroi një postim të ri me titullin \u201e{postTitle}\u201c në një nga grupet tuaja. Klikoni butonin për të parë këtë postim:",
|
|
||||||
"userJoinedGroup": " u bashkua me grupin \u201e{groupName}\u201c. Klikoni butonin për të parë këtë grup:",
|
|
||||||
"userLeftGroup": " u largua nga grupi \u201e{groupName}\u201c. Klikoni butonin për të parë këtë grup:"
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"notification": "Сповіщення",
|
|
||||||
"subjects": {
|
|
||||||
"changedGroupMemberRole": "Роль у групі змінено",
|
|
||||||
"chatMessage": "Нове повідомлення в чаті",
|
|
||||||
"commentedOnPost": "Новий коментар до публікації",
|
|
||||||
"followedUserPosted": "Нова публікація від користувача, на якого ви підписані",
|
|
||||||
"mentionedInComment": "Згадка в коментарі",
|
|
||||||
"mentionedInPost": "Згадка в публікації",
|
|
||||||
"newEmail": "Нова адреса електронної пошти",
|
|
||||||
"removedUserFromGroup": "Видалено з групи",
|
|
||||||
"postInGroup": "Нова публікація в групі",
|
|
||||||
"resetPassword": "Скинути пароль",
|
|
||||||
"userJoinedGroup": "Користувач приєднався до групи",
|
|
||||||
"userLeftGroup": "Користувач покинув групу",
|
|
||||||
"wrongEmail": "Невірна адреса електронної пошти?"
|
|
||||||
},
|
|
||||||
"registration": {
|
|
||||||
"introduction": "Дякуємо за реєстрацію – ми раді, що ви з нами. Залишився лише один маленький крок, перш ніж ми зможемо разом змінювати світ … Будь ласка, підтвердіть свою адресу електронної пошти, натиснувши кнопку нижче:",
|
|
||||||
"codeHint": "Якщо кнопка вище не працює, ви також можете скопіювати наступний код у вікно браузера: ",
|
|
||||||
"codeHintException": "Однак це працює лише в тому випадку, якщо ви зареєструвалися через наш сайт.",
|
|
||||||
"notYouStart": "Якщо ви не реєструвалися на ",
|
|
||||||
"notYouEnd": " рекомендуємо заглянути! Це соціальна мережа від людей для людей, які хочуть об'єднуватися і разом змінювати світ.",
|
|
||||||
"ps": "PS: Якщо ви проігноруєте цей лист, ми не створимо для вас обліковий запис. ;)"
|
|
||||||
},
|
|
||||||
"emailVerification": {
|
|
||||||
"codeHint": "Якщо кнопка вище не працює, ви також можете скопіювати наступний код у вікно браузера: ",
|
|
||||||
"introduction": "Хочете змінити свою адресу електронної пошти? Без проблем! Просто натисніть кнопку нижче, щоб підтвердити нову адресу:",
|
|
||||||
"doNotChange": "Якщо ви не хочете змінювати свою адресу електронної пошти, просто проігноруйте це повідомлення. "
|
|
||||||
},
|
|
||||||
"support": "Якщо у вас є запитання або проблеми, зверніться до нашої служби підтримки: ",
|
|
||||||
"buttons": {
|
|
||||||
"confirmEmail": "Підтвердіть свою адресу електронної пошти",
|
|
||||||
"resetPassword": "Скинути пароль",
|
|
||||||
"tryAgain": "Спробувати іншу адресу електронної пошти",
|
|
||||||
"verifyEmail": "Підтвердити адресу електронної пошти",
|
|
||||||
"viewChat": "Відкрити чат",
|
|
||||||
"viewComment": "Переглянути коментар",
|
|
||||||
"viewGroup": "Переглянути групу",
|
|
||||||
"viewPost": "Переглянути публікацію"
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"greeting": "Вітаємо",
|
|
||||||
"seeYou": "До зустрічі на ",
|
|
||||||
"yourTeam": "– Команда {team}",
|
|
||||||
"settingsHint": "PS: Якщо ви більше не хочете отримувати електронні листи, змініть свої ",
|
|
||||||
"settingsName": "налаштування сповіщень",
|
|
||||||
"welcome": "Ласкаво просимо до"
|
|
||||||
},
|
|
||||||
"resetPassword": {
|
|
||||||
"codeHint": "Якщо кнопка вище не працює, ви також можете скопіювати наступний код у вікно браузера: ",
|
|
||||||
"ignore": "Якщо ви не запитували новий пароль, просто проігноруйте цей лист.",
|
|
||||||
"introduction": "Забули пароль? Без проблем! Просто натисніть кнопку нижче, щоб скинути його протягом наступних 24 годин:"
|
|
||||||
},
|
|
||||||
"wrongEmail": {
|
|
||||||
"ignoreEnd": " або якщо ви не хотіли скидати пароль, просто проігноруйте цей лист.",
|
|
||||||
"ignoreStart": "Якщо у вас немає облікового запису на ",
|
|
||||||
"introduction": "Ви запросили скидання пароля, але, на жаль, ми не знайшли облікового запису, пов'язаного з вашою адресою електронної пошти. Можливо, ви зареєструвалися з іншою адресою?"
|
|
||||||
},
|
|
||||||
"changedGroupMemberRole": "вашу роль у групі «{groupName}» було змінено. Натисніть кнопку, щоб переглянути цю групу:",
|
|
||||||
"chatMessageStart": "ви отримали нове повідомлення в чаті від ",
|
|
||||||
"chatMessageEnd": ".",
|
|
||||||
"commentedOnPost": " прокоментував публікацію, за якою ви стежите, з заголовком «{postTitle}». Натисніть кнопку, щоб переглянути цей коментар:",
|
|
||||||
"followedUserPosted": ", користувач, на якого ви підписані, написав нову публікацію з заголовком «{postTitle}». Натисніть кнопку, щоб переглянути цю публікацію:",
|
|
||||||
"mentionedInComment": " згадав вас у коментарі до публікації з заголовком «{postTitle}». Натисніть кнопку, щоб переглянути цей коментар:",
|
|
||||||
"mentionedInPost": " згадав вас у публікації з заголовком «{postTitle}». Натисніть кнопку, щоб переглянути цю публікацію:",
|
|
||||||
"removedUserFromGroup": "вас було видалено з групи «{groupName}».",
|
|
||||||
"postInGroup": "хтось написав нову публікацію з заголовком «{postTitle}» в одній з ваших груп. Натисніть кнопку, щоб переглянути цю публікацію:",
|
|
||||||
"userJoinedGroup": " приєднався до групи «{groupName}». Натисніть кнопку, щоб переглянути цю групу:",
|
|
||||||
"userLeftGroup": " покинув групу «{groupName}». Натисніть кнопку, щоб переглянути цю групу:"
|
|
||||||
}
|
|
||||||
@ -39,7 +39,7 @@ const email = new Email({
|
|||||||
},
|
},
|
||||||
transport,
|
transport,
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ['en', 'de', 'nl', 'fr', 'it', 'es', 'pt', 'pl', 'ru', 'sq', 'uk'],
|
locales: ['en', 'de'],
|
||||||
defaultLocale: CONFIG.LANGUAGE_DEFAULT,
|
defaultLocale: CONFIG.LANGUAGE_DEFAULT,
|
||||||
retryInDefaultLocale: false,
|
retryInDefaultLocale: false,
|
||||||
directory: path.join(__dirname, 'locales'),
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ mutation DeleteComment($id: ID!) {
|
|||||||
DeleteComment(id: $id) {
|
DeleteComment(id: $id) {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,8 @@ mutation CreateGroup(
|
|||||||
location {
|
location {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
nameDE
|
||||||
|
nameEN
|
||||||
}
|
}
|
||||||
myRole
|
myRole
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ query Group($isMember: Boolean, $id: ID, $slug: String) {
|
|||||||
location {
|
location {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
nameDE
|
||||||
|
nameEN
|
||||||
}
|
}
|
||||||
myRole
|
myRole
|
||||||
inviteCodes {
|
inviteCodes {
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
query GroupWithLocationFilter($hasLocation: Boolean) {
|
|
||||||
Group(hasLocation: $hasLocation) {
|
|
||||||
id
|
|
||||||
location {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -43,6 +43,8 @@ mutation UpdateGroup(
|
|||||||
location {
|
location {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
nameDE
|
||||||
|
nameEN
|
||||||
}
|
}
|
||||||
myRole
|
myRole
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
mutation CreateMessage($roomId: ID, $userId: ID, $content: String, $files: [FileInput]) {
|
mutation CreateMessage($roomId: ID!, $content: String!, $files: [FileInput]) {
|
||||||
CreateMessage(roomId: $roomId, userId: $userId, content: $content, files: $files) {
|
CreateMessage(roomId: $roomId, content: $content, files: $files) {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
senderId
|
senderId
|
||||||
username
|
username
|
||||||
avatar
|
avatar
|
||||||
date
|
date
|
||||||
room {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
saved
|
saved
|
||||||
distributed
|
distributed
|
||||||
seen
|
seen
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
mutation CreateGroupRoom($groupId: ID!) {
|
mutation CreateRoom($userId: ID!) {
|
||||||
CreateGroupRoom(groupId: $groupId) {
|
CreateRoom(userId: $userId) {
|
||||||
id
|
id
|
||||||
roomId
|
roomId
|
||||||
roomName
|
roomName
|
||||||
isGroupRoom
|
|
||||||
lastMessageAt
|
lastMessageAt
|
||||||
unreadCount
|
unreadCount
|
||||||
|
#avatar
|
||||||
users {
|
users {
|
||||||
_id
|
_id
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
query Message($roomId: ID!, $first: Int, $offset: Int, $beforeIndex: Int) {
|
query Message($roomId: ID!, $first: Int, $offset: Int) {
|
||||||
Message(roomId: $roomId, first: $first, offset: $offset, beforeIndex: $beforeIndex, orderBy: indexId_desc) {
|
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
|
||||||
_id
|
_id
|
||||||
id
|
id
|
||||||
indexId
|
indexId
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
query Room($first: Int, $before: String, $id: ID, $userId: ID, $groupId: ID) {
|
query Room($first: Int, $offset: Int, $id: ID) {
|
||||||
Room(first: $first, before: $before, id: $id, userId: $userId, groupId: $groupId) {
|
Room(first: $first, offset: $offset, id: $id, orderBy: lastMessageAt_desc) {
|
||||||
id
|
id
|
||||||
roomId
|
roomId
|
||||||
roomName
|
roomName
|
||||||
|
|||||||
@ -3,6 +3,7 @@ mutation DeletePost($id: ID!) {
|
|||||||
id
|
id
|
||||||
deleted
|
deleted
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
image {
|
image {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
@ -10,6 +11,7 @@ mutation DeletePost($id: ID!) {
|
|||||||
id
|
id
|
||||||
deleted
|
deleted
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ query Post($id: ID, $filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [
|
|||||||
id
|
id
|
||||||
title
|
title
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
eventStart
|
eventStart
|
||||||
pinned
|
pinned
|
||||||
createdAt
|
createdAt
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
query PostWithLocationFilter($filter: _PostFilter) {
|
|
||||||
Post(filter: $filter) {
|
|
||||||
id
|
|
||||||
eventLocation {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,16 +7,19 @@ mutation DeleteUser($id: ID!, $resource: [Deletable]) {
|
|||||||
contributions {
|
contributions {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
deleted
|
deleted
|
||||||
comments {
|
comments {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comments {
|
comments {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,9 @@ mutation UpdateUser(
|
|||||||
location {
|
location {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
nameDE
|
||||||
|
nameEN
|
||||||
|
nameRU
|
||||||
}
|
}
|
||||||
emailNotificationSettings {
|
emailNotificationSettings {
|
||||||
type
|
type
|
||||||
|
|||||||
@ -29,6 +29,7 @@ query User($id: ID, $name: String, $email: String) {
|
|||||||
comments {
|
comments {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
contributions {
|
contributions {
|
||||||
id
|
id
|
||||||
@ -38,6 +39,7 @@ query User($id: ID, $name: String, $email: String) {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMuted
|
isMuted
|
||||||
|
|||||||
@ -30,6 +30,7 @@ query UserEmail($id: ID, $name: String, $email: String) {
|
|||||||
comments {
|
comments {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
contributions {
|
contributions {
|
||||||
id
|
id
|
||||||
@ -39,6 +40,7 @@ query UserEmail($id: ID, $name: String, $email: String) {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMuted
|
isMuted
|
||||||
|
|||||||
@ -29,6 +29,7 @@ query UserEmailNotificationSettings($id: ID, $name: String, $email: String) {
|
|||||||
comments {
|
comments {
|
||||||
id
|
id
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
contributions {
|
contributions {
|
||||||
id
|
id
|
||||||
@ -38,6 +39,7 @@ query UserEmailNotificationSettings($id: ID, $name: String, $email: String) {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
content
|
content
|
||||||
|
contentExcerpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMuted
|
isMuted
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
query UserWithLocationFilter($filter: _UserFilter) {
|
|
||||||
User(filter: $filter) {
|
|
||||||
id
|
|
||||||
location {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,6 +12,7 @@ import { Upload } from '@aws-sdk/lib-storage'
|
|||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
import { UserInputError } from '@graphql/errors'
|
import { UserInputError } from '@graphql/errors'
|
||||||
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
||||||
|
import CreateRoom from '@graphql/queries/messaging/CreateRoom.gql'
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
|
||||||
import { attachments } from './attachments'
|
import { attachments } from './attachments'
|
||||||
@ -93,14 +94,12 @@ describe('delete Attachment', () => {
|
|||||||
chatPartner = await u2.toJson()
|
chatPartner = await u2.toJson()
|
||||||
|
|
||||||
authenticatedUser = user
|
authenticatedUser = user
|
||||||
const initResult = await mutate({
|
const { data: room } = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: chatPartner.id,
|
userId: chatPartner.id,
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const roomId = initResult.data.CreateMessage.room.id
|
|
||||||
|
|
||||||
const f = await Factory.build('file', {
|
const f = await Factory.build('file', {
|
||||||
url: 'http://localhost/some/file/url/',
|
url: 'http://localhost/some/file/url/',
|
||||||
@ -112,7 +111,7 @@ describe('delete Attachment', () => {
|
|||||||
const m = await mutate({
|
const m = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
roomId,
|
roomId: room?.CreateRoom.id,
|
||||||
content: 'test messsage',
|
content: 'test messsage',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -240,6 +240,7 @@ describe('DeleteComment', () => {
|
|||||||
id: 'c456',
|
id: 'c456',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expect(data).toMatchObject(expected)
|
expect(data).toMatchObject(expected)
|
||||||
|
|||||||
@ -83,6 +83,7 @@ export default {
|
|||||||
MATCH (comment:Comment {id: $commentId})
|
MATCH (comment:Comment {id: $commentId})
|
||||||
SET comment.deleted = TRUE
|
SET comment.deleted = TRUE
|
||||||
SET comment.content = 'UNAVAILABLE'
|
SET comment.content = 'UNAVAILABLE'
|
||||||
|
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||||
RETURN comment
|
RETURN comment
|
||||||
`,
|
`,
|
||||||
{ commentId: args.id },
|
{ commentId: args.id },
|
||||||
|
|||||||
@ -302,6 +302,8 @@ describe('in mode', () => {
|
|||||||
locationName: 'Hamburg, Germany',
|
locationName: 'Hamburg, Germany',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Hamburg',
|
name: 'Hamburg',
|
||||||
|
nameDE: 'Hamburg',
|
||||||
|
nameEN: 'Hamburg',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -549,6 +551,8 @@ describe('in mode', () => {
|
|||||||
locationName: 'Hamburg, Germany',
|
locationName: 'Hamburg, Germany',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Hamburg',
|
name: 'Hamburg',
|
||||||
|
nameDE: 'Hamburg',
|
||||||
|
nameEN: 'Hamburg',
|
||||||
}),
|
}),
|
||||||
myRole: 'owner',
|
myRole: 'owner',
|
||||||
}),
|
}),
|
||||||
@ -2891,6 +2895,8 @@ describe('in mode', () => {
|
|||||||
locationName: 'Berlin, Germany',
|
locationName: 'Berlin, Germany',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Berlin',
|
name: 'Berlin',
|
||||||
|
nameDE: 'Berlin',
|
||||||
|
nameEN: 'Berlin',
|
||||||
}),
|
}),
|
||||||
myRole: 'owner',
|
myRole: 'owner',
|
||||||
},
|
},
|
||||||
@ -2941,6 +2947,8 @@ describe('in mode', () => {
|
|||||||
locationName: 'Paris, France',
|
locationName: 'Paris, France',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Paris',
|
name: 'Paris',
|
||||||
|
nameDE: 'Paris',
|
||||||
|
nameEN: 'Paris',
|
||||||
}),
|
}),
|
||||||
myRole: 'owner',
|
myRole: 'owner',
|
||||||
},
|
},
|
||||||
@ -2967,6 +2975,8 @@ describe('in mode', () => {
|
|||||||
locationName: 'Hamburg, Germany',
|
locationName: 'Hamburg, Germany',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Hamburg',
|
name: 'Hamburg',
|
||||||
|
nameDE: 'Hamburg',
|
||||||
|
nameEN: 'Hamburg',
|
||||||
}),
|
}),
|
||||||
myRole: 'owner',
|
myRole: 'owner',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import type { Context } from '@src/context'
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
Group: async (_object, params, context: Context, _resolveInfo) => {
|
Group: async (_object, params, context: Context, _resolveInfo) => {
|
||||||
const { isMember, hasLocation, id, slug, first, offset } = params
|
const { isMember, id, slug, first, offset } = params
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
try {
|
try {
|
||||||
return await session.readTransaction(async (txc) => {
|
return await session.readTransaction(async (txc) => {
|
||||||
@ -35,13 +35,10 @@ export default {
|
|||||||
if (slug !== undefined) matchFilters.push('group.slug = $slug')
|
if (slug !== undefined) matchFilters.push('group.slug = $slug')
|
||||||
const matchWhere = matchFilters.length ? `WHERE ${matchFilters.join(' AND ')}` : ''
|
const matchWhere = matchFilters.length ? `WHERE ${matchFilters.join(' AND ')}` : ''
|
||||||
|
|
||||||
const locationMatch = hasLocation === true ? 'MATCH (group)-[:IS_IN]->(:Location)' : ''
|
|
||||||
|
|
||||||
const transactionResponse = await txc.run(
|
const transactionResponse = await txc.run(
|
||||||
`
|
`
|
||||||
MATCH (group:Group)
|
MATCH (group:Group)
|
||||||
${matchWhere}
|
${matchWhere}
|
||||||
${locationMatch}
|
|
||||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||||
WITH group, membership
|
WITH group, membership
|
||||||
${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||||
@ -289,14 +286,9 @@ export default {
|
|||||||
RETURN user {.*}, membership {.*}
|
RETURN user {.*}, membership {.*}
|
||||||
`
|
`
|
||||||
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
||||||
const records = transactionResponse.records.map((record) => {
|
return transactionResponse.records.map((record) => {
|
||||||
return { user: record.get('user'), membership: record.get('membership') }
|
return { user: record.get('user'), membership: record.get('membership') }
|
||||||
})
|
})
|
||||||
// Add user to group chat room if they are an active member (not pending)
|
|
||||||
if (records[0]?.membership?.role && records[0].membership.role !== 'pending') {
|
|
||||||
await addUserToGroupChatRoom(transaction, groupId, userId)
|
|
||||||
}
|
|
||||||
return records
|
|
||||||
})
|
})
|
||||||
if (!result[0]) {
|
if (!result[0]) {
|
||||||
throw new UserInputError('Could not find User or Group')
|
throw new UserInputError('Could not find User or Group')
|
||||||
@ -356,12 +348,6 @@ export default {
|
|||||||
const [member] = transactionResponse.records.map((record) => {
|
const [member] = transactionResponse.records.map((record) => {
|
||||||
return { user: record.get('user'), membership: record.get('membership') }
|
return { user: record.get('user'), membership: record.get('membership') }
|
||||||
})
|
})
|
||||||
// Manage group chat room membership based on role
|
|
||||||
if (['usual', 'admin', 'owner'].includes(roleInGroup)) {
|
|
||||||
await addUserToGroupChatRoom(transaction, groupId, userId)
|
|
||||||
} else {
|
|
||||||
await removeUserFromGroupChatRoom(transaction, groupId, userId)
|
|
||||||
}
|
|
||||||
return member
|
return member
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@ -517,29 +503,6 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const addUserToGroupChatRoom = async (transaction, groupId, userId) => {
|
|
||||||
await transaction.run(
|
|
||||||
`
|
|
||||||
OPTIONAL MATCH (room:Room)-[:ROOM_FOR]->(group:Group {id: $groupId})
|
|
||||||
WITH room
|
|
||||||
WHERE room IS NOT NULL
|
|
||||||
MATCH (user:User {id: $userId})
|
|
||||||
MERGE (user)-[:CHATS_IN]->(room)
|
|
||||||
`,
|
|
||||||
{ groupId, userId },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeUserFromGroupChatRoom = async (transaction, groupId, userId) => {
|
|
||||||
await transaction.run(
|
|
||||||
`
|
|
||||||
OPTIONAL MATCH (user:User {id: $userId})-[chatsIn:CHATS_IN]->(room:Room)-[:ROOM_FOR]->(group:Group {id: $groupId})
|
|
||||||
DELETE chatsIn
|
|
||||||
`,
|
|
||||||
{ groupId, userId },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => {
|
const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => {
|
||||||
return session.writeTransaction(async (transaction) => {
|
return session.writeTransaction(async (transaction) => {
|
||||||
const removeUserFromGroupCypher = `
|
const removeUserFromGroupCypher = `
|
||||||
@ -565,8 +528,6 @@ const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId)
|
|||||||
if (!result) {
|
if (!result) {
|
||||||
throw new UserInputError('User is not a member of this group')
|
throw new UserInputError('User is not a member of this group')
|
||||||
}
|
}
|
||||||
// Remove user from group chat room
|
|
||||||
await removeUserFromGroupChatRoom(transaction, groupId, userId)
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
import { removeUndefinedNullValuesFromObject, convertObjectToCypherMapLiteral } from './Resolver'
|
|
||||||
|
|
||||||
describe('removeUndefinedNullValuesFromObject', () => {
|
|
||||||
it('removes undefined values', () => {
|
|
||||||
const obj = { a: 1, b: undefined, c: 'hello' }
|
|
||||||
removeUndefinedNullValuesFromObject(obj)
|
|
||||||
expect(obj).toEqual({ a: 1, c: 'hello' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes null values', () => {
|
|
||||||
const obj = { a: 1, b: null, c: 'hello' }
|
|
||||||
removeUndefinedNullValuesFromObject(obj)
|
|
||||||
expect(obj).toEqual({ a: 1, c: 'hello' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('keeps falsy but defined values', () => {
|
|
||||||
const obj = { a: 0, b: false, c: '' }
|
|
||||||
removeUndefinedNullValuesFromObject(obj)
|
|
||||||
expect(obj).toEqual({ a: 0, b: false, c: '' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('handles empty object', () => {
|
|
||||||
const obj = {}
|
|
||||||
removeUndefinedNullValuesFromObject(obj)
|
|
||||||
expect(obj).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('convertObjectToCypherMapLiteral', () => {
|
|
||||||
it('converts single entry', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' })).toBe('{id: "g0"}')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('converts multiple entries', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({ id: 'g0', slug: 'yoga' })).toBe(
|
|
||||||
'{id: "g0", slug: "yoga"}',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns empty string for empty object', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({})).toBe('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds space in front when addSpaceInfrontIfMapIsNotEmpty is true and map is not empty', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' }, true)).toBe(' {id: "g0"}')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not add space when addSpaceInfrontIfMapIsNotEmpty is true but map is empty', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({}, true)).toBe('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not add space when addSpaceInfrontIfMapIsNotEmpty is false', () => {
|
|
||||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' }, false)).toBe('{id: "g0"}')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
|
||||||
import GroupWithLocationFilter from '@graphql/queries/groups/GroupWithLocationFilter.gql'
|
|
||||||
import PostWithLocationFilter from '@graphql/queries/posts/PostWithLocationFilter.gql'
|
|
||||||
import UserWithLocationFilter from '@graphql/queries/users/UserWithLocationFilter.gql'
|
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
|
||||||
|
|
||||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
|
||||||
import type { Context } from '@src/context'
|
|
||||||
|
|
||||||
let authenticatedUser: Context['user']
|
|
||||||
const context = () => ({ authenticatedUser })
|
|
||||||
let query: ApolloTestSetup['query']
|
|
||||||
let database: ApolloTestSetup['database']
|
|
||||||
let server: ApolloTestSetup['server']
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
const apolloSetup = await createApolloTestSetup({ context })
|
|
||||||
query = apolloSetup.query
|
|
||||||
database = apolloSetup.database
|
|
||||||
server = apolloSetup.server
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
void server.stop()
|
|
||||||
void database.driver.close()
|
|
||||||
database.neode.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('hasLocation filter', () => {
|
|
||||||
describe('User', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const location = await Factory.build('location', {
|
|
||||||
id: 'loc-hamburg',
|
|
||||||
name: 'Hamburg',
|
|
||||||
type: 'region',
|
|
||||||
lng: 10.0,
|
|
||||||
lat: 53.55,
|
|
||||||
})
|
|
||||||
const userWithLocation = await Factory.build('user', {
|
|
||||||
id: 'u-with-loc',
|
|
||||||
name: 'User With Location',
|
|
||||||
})
|
|
||||||
await userWithLocation.relateTo(location, 'isIn')
|
|
||||||
await Factory.build('user', {
|
|
||||||
id: 'u-without-loc',
|
|
||||||
name: 'User Without Location',
|
|
||||||
})
|
|
||||||
authenticatedUser = await userWithLocation.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns all users without filter', async () => {
|
|
||||||
const result = await query({ query: UserWithLocationFilter })
|
|
||||||
expect(result.data?.User.length).toBeGreaterThanOrEqual(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns only users with location when hasLocation is true', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: UserWithLocationFilter,
|
|
||||||
variables: { filter: { hasLocation: true } },
|
|
||||||
})
|
|
||||||
const ids = result.data?.User.map((u: { id: string }) => u.id)
|
|
||||||
expect(ids).toContain('u-with-loc')
|
|
||||||
expect(ids).not.toContain('u-without-loc')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Group', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const location = await Factory.build('location', {
|
|
||||||
id: 'loc-berlin',
|
|
||||||
name: 'Berlin',
|
|
||||||
type: 'region',
|
|
||||||
lng: 13.4,
|
|
||||||
lat: 52.52,
|
|
||||||
})
|
|
||||||
const owner = await Factory.build('user', { id: 'group-owner', name: 'Owner' })
|
|
||||||
authenticatedUser = await owner.toJson()
|
|
||||||
|
|
||||||
const groupWithLocation = await Factory.build('group', {
|
|
||||||
id: 'g-with-loc',
|
|
||||||
name: 'Group With Location',
|
|
||||||
groupType: 'public',
|
|
||||||
ownerId: 'group-owner',
|
|
||||||
})
|
|
||||||
await groupWithLocation.relateTo(location, 'isIn')
|
|
||||||
|
|
||||||
await Factory.build('group', {
|
|
||||||
id: 'g-without-loc',
|
|
||||||
name: 'Group Without Location',
|
|
||||||
groupType: 'public',
|
|
||||||
ownerId: 'group-owner',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns all groups without filter', async () => {
|
|
||||||
const result = await query({ query: GroupWithLocationFilter })
|
|
||||||
expect(result.data?.Group.length).toBeGreaterThanOrEqual(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns only groups with location when hasLocation is true', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: GroupWithLocationFilter,
|
|
||||||
variables: { hasLocation: true },
|
|
||||||
})
|
|
||||||
const ids = result.data?.Group.map((g: { id: string }) => g.id)
|
|
||||||
expect(ids).toContain('g-with-loc')
|
|
||||||
expect(ids).not.toContain('g-without-loc')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Post', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const author = await Factory.build('user', { id: 'post-author', name: 'Author' })
|
|
||||||
authenticatedUser = await author.toJson()
|
|
||||||
|
|
||||||
await Factory.build('location', {
|
|
||||||
id: 'loc-munich',
|
|
||||||
name: 'Munich',
|
|
||||||
type: 'region',
|
|
||||||
lng: 11.58,
|
|
||||||
lat: 48.14,
|
|
||||||
})
|
|
||||||
await Factory.build('post', {
|
|
||||||
id: 'p-with-loc',
|
|
||||||
title: 'Event With Location',
|
|
||||||
postType: 'Event',
|
|
||||||
authorId: 'post-author',
|
|
||||||
})
|
|
||||||
// Post model has no isIn relationship defined in Neode, use Cypher directly
|
|
||||||
const session = database.driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((txc) =>
|
|
||||||
txc.run(`
|
|
||||||
MATCH (p:Post {id: 'p-with-loc'}), (l:Location {id: 'loc-munich'})
|
|
||||||
MERGE (p)-[:IS_IN]->(l)
|
|
||||||
`),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
await Factory.build('post', {
|
|
||||||
id: 'p-without-loc',
|
|
||||||
title: 'Event Without Location',
|
|
||||||
postType: 'Event',
|
|
||||||
authorId: 'post-author',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns all posts without hasLocation filter', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: PostWithLocationFilter,
|
|
||||||
})
|
|
||||||
expect(result.data?.Post.length).toBeGreaterThanOrEqual(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns only posts with location when hasLocation is true', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: PostWithLocationFilter,
|
|
||||||
variables: { filter: { hasLocation: true } },
|
|
||||||
})
|
|
||||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
|
||||||
expect(ids).toContain('p-with-loc')
|
|
||||||
expect(ids).not.toContain('p-without-loc')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import type { Context } from '@src/context'
|
|
||||||
|
|
||||||
interface FilterParams {
|
|
||||||
filter?: {
|
|
||||||
hasLocation?: boolean
|
|
||||||
id_in?: string[]
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type AllowedLabel = 'User' | 'Post'
|
|
||||||
|
|
||||||
const getIdsWithLocation = async (context: Context, label: AllowedLabel): Promise<string[]> => {
|
|
||||||
const session = context.driver.session()
|
|
||||||
try {
|
|
||||||
const result = await session.readTransaction(async (transaction) => {
|
|
||||||
const cypher = `
|
|
||||||
MATCH (n:${label})-[:IS_IN]->(l:Location)
|
|
||||||
RETURN collect(n.id) AS ids`
|
|
||||||
const response = await transaction.run(cypher)
|
|
||||||
return response.records.map((record) => record.get('ids') as string[])
|
|
||||||
})
|
|
||||||
const [ids] = result
|
|
||||||
return ids
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeIdIn = (existing: string[] | undefined, incoming: string[]): string[] => {
|
|
||||||
if (!existing) return incoming
|
|
||||||
return existing.filter((id) => incoming.includes(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const filterUsersHasLocation = async (
|
|
||||||
params: FilterParams,
|
|
||||||
context: Context,
|
|
||||||
): Promise<FilterParams> => {
|
|
||||||
if (!params.filter?.hasLocation) return params
|
|
||||||
delete params.filter.hasLocation
|
|
||||||
const userIds = await getIdsWithLocation(context, 'User')
|
|
||||||
params.filter.id_in = mergeIdIn(params.filter.id_in, userIds)
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
export const filterPostsHasLocation = async (
|
|
||||||
params: FilterParams,
|
|
||||||
context: Context,
|
|
||||||
): Promise<FilterParams> => {
|
|
||||||
if (!params.filter?.hasLocation) return params
|
|
||||||
delete params.filter.hasLocation
|
|
||||||
const postIds = await getIdsWithLocation(context, 'Post')
|
|
||||||
params.filter.id_in = mergeIdIn(params.filter.id_in, postIds)
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
|
||||||
import CreatePost from '@graphql/queries/posts/CreatePost.gql'
|
|
||||||
import Post from '@graphql/queries/posts/Post.gql'
|
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
|
||||||
|
|
||||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
|
||||||
import type { Context } from '@src/context'
|
|
||||||
|
|
||||||
let authenticatedUser: Context['user']
|
|
||||||
const context = () => ({ authenticatedUser })
|
|
||||||
let query: ApolloTestSetup['query']
|
|
||||||
let mutate: ApolloTestSetup['mutate']
|
|
||||||
let database: ApolloTestSetup['database']
|
|
||||||
let server: ApolloTestSetup['server']
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
const apolloSetup = await createApolloTestSetup({ context })
|
|
||||||
query = apolloSetup.query
|
|
||||||
mutate = apolloSetup.mutate
|
|
||||||
database = apolloSetup.database
|
|
||||||
server = apolloSetup.server
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
void server.stop()
|
|
||||||
void database.driver.close()
|
|
||||||
database.neode.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('filterForMutedUsers', () => {
|
|
||||||
describe('when looking up a single post by id', () => {
|
|
||||||
it('does not filter muted users', async () => {
|
|
||||||
const author = await Factory.build('user', { id: 'muted-author', name: 'Muted Author' })
|
|
||||||
const viewer = await Factory.build('user', { id: 'viewer', name: 'Viewer' })
|
|
||||||
|
|
||||||
// Viewer mutes author
|
|
||||||
const session = database.driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((txc) =>
|
|
||||||
txc.run(`
|
|
||||||
MATCH (viewer:User {id: 'viewer'}), (author:User {id: 'muted-author'})
|
|
||||||
MERGE (viewer)-[:MUTED]->(author)
|
|
||||||
`),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author creates a post
|
|
||||||
authenticatedUser = await author.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'muted-post',
|
|
||||||
title: 'Post by muted user',
|
|
||||||
content: 'Some content here for the post',
|
|
||||||
postType: 'Article',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Viewer queries for the specific post by id — should still see it
|
|
||||||
authenticatedUser = await viewer.toJson()
|
|
||||||
const result = await query({
|
|
||||||
query: Post,
|
|
||||||
variables: { id: 'muted-post' },
|
|
||||||
})
|
|
||||||
expect(result.data?.Post).toHaveLength(1)
|
|
||||||
expect(result.data?.Post[0].id).toBe('muted-post')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when listing all posts', () => {
|
|
||||||
it('filters posts from muted users', async () => {
|
|
||||||
const author = await Factory.build('user', { id: 'muted-author', name: 'Muted Author' })
|
|
||||||
const otherAuthor = await Factory.build('user', {
|
|
||||||
id: 'other-author',
|
|
||||||
name: 'Other Author',
|
|
||||||
})
|
|
||||||
const viewer = await Factory.build('user', { id: 'viewer', name: 'Viewer' })
|
|
||||||
|
|
||||||
// Viewer mutes author
|
|
||||||
const session = database.driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((txc) =>
|
|
||||||
txc.run(`
|
|
||||||
MATCH (viewer:User {id: 'viewer'}), (author:User {id: 'muted-author'})
|
|
||||||
MERGE (viewer)-[:MUTED]->(author)
|
|
||||||
`),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both authors create posts
|
|
||||||
authenticatedUser = await author.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'muted-post',
|
|
||||||
title: 'Post by muted user',
|
|
||||||
content: 'Some content here for the muted post',
|
|
||||||
postType: 'Article',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
authenticatedUser = await otherAuthor.toJson()
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'visible-post',
|
|
||||||
title: 'Post by other user',
|
|
||||||
content: 'Some content here for the visible post',
|
|
||||||
postType: 'Article',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Viewer lists all posts — should not see muted author's post
|
|
||||||
authenticatedUser = await viewer.toJson()
|
|
||||||
const result = await query({ query: Post })
|
|
||||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
|
||||||
expect(ids).toContain('visible-post')
|
|
||||||
expect(ids).not.toContain('muted-post')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('filterPostsOfMyGroups', () => {
|
|
||||||
describe('when user has no group memberships', () => {
|
|
||||||
it('returns empty for postsInMyGroups filter', async () => {
|
|
||||||
const author = await Factory.build('user', { id: 'author', name: 'Author' })
|
|
||||||
authenticatedUser = await author.toJson()
|
|
||||||
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'some-post',
|
|
||||||
title: 'A regular post',
|
|
||||||
content: 'Some content here for the post',
|
|
||||||
postType: 'Article',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Query with postsInMyGroups but user has no groups
|
|
||||||
const result = await query({
|
|
||||||
query: Post,
|
|
||||||
variables: { filter: { postsInMyGroups: true } },
|
|
||||||
})
|
|
||||||
expect(result.data?.Post).toHaveLength(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('filterInvisiblePosts', () => {
|
|
||||||
describe('with closed group posts', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const owner = await Factory.build('user', { id: 'group-owner', name: 'Group Owner' })
|
|
||||||
await Factory.build('user', { id: 'outsider', name: 'Outsider' })
|
|
||||||
|
|
||||||
authenticatedUser = await owner.toJson()
|
|
||||||
await Factory.build('group', {
|
|
||||||
id: 'closed-group',
|
|
||||||
name: 'Closed Group',
|
|
||||||
groupType: 'closed',
|
|
||||||
ownerId: 'group-owner',
|
|
||||||
})
|
|
||||||
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'closed-group-post',
|
|
||||||
title: 'Secret Post',
|
|
||||||
content: 'Some content here for the secret post',
|
|
||||||
postType: 'Article',
|
|
||||||
groupId: 'closed-group',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await mutate({
|
|
||||||
mutation: CreatePost,
|
|
||||||
variables: {
|
|
||||||
id: 'public-post',
|
|
||||||
title: 'Public Post',
|
|
||||||
content: 'Some content here for the public post',
|
|
||||||
postType: 'Article',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('filters posts in non-public groups for non-members', async () => {
|
|
||||||
const outsider = await database.neode.find('User', 'outsider')
|
|
||||||
authenticatedUser = (await outsider.toJson()) as Context['user']
|
|
||||||
const result = await query({ query: Post })
|
|
||||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
|
||||||
expect(ids).toContain('public-post')
|
|
||||||
expect(ids).not.toContain('closed-group-post')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,29 +1,17 @@
|
|||||||
/* 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 { parse } from 'graphql'
|
|
||||||
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
|
import UpdateUser from '@graphql/queries/users/UpdateUser.gql'
|
||||||
import User from '@graphql/queries/users/User.gql'
|
import User from '@graphql/queries/users/User.gql'
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
|
||||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
const UserLocationName = parse(`
|
|
||||||
query User($id: ID, $lang: String) {
|
|
||||||
User(id: $id) {
|
|
||||||
id
|
|
||||||
location {
|
|
||||||
id
|
|
||||||
name(lang: $lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
let authenticatedUser: Context['user']
|
let authenticatedUser: Context['user']
|
||||||
const context = () => ({ authenticatedUser })
|
const context = () => ({ authenticatedUser })
|
||||||
|
let mutate: ApolloTestSetup['mutate']
|
||||||
let query: ApolloTestSetup['query']
|
let query: ApolloTestSetup['query']
|
||||||
let database: ApolloTestSetup['database']
|
let database: ApolloTestSetup['database']
|
||||||
let server: ApolloTestSetup['server']
|
let server: ApolloTestSetup['server']
|
||||||
@ -31,6 +19,7 @@ let server: ApolloTestSetup['server']
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
const apolloSetup = await createApolloTestSetup({ context })
|
const apolloSetup = await createApolloTestSetup({ context })
|
||||||
|
mutate = apolloSetup.mutate
|
||||||
query = apolloSetup.query
|
query = apolloSetup.query
|
||||||
database = apolloSetup.database
|
database = apolloSetup.database
|
||||||
server = apolloSetup.server
|
server = apolloSetup.server
|
||||||
@ -50,84 +39,41 @@ afterEach(async () => {
|
|||||||
|
|
||||||
describe('resolvers', () => {
|
describe('resolvers', () => {
|
||||||
describe('Location', () => {
|
describe('Location', () => {
|
||||||
describe('name(lang)', () => {
|
describe('custom mutation, not handled by neo4j-graphql-js', () => {
|
||||||
|
let variables
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const Hamburg = await Factory.build('location', {
|
variables = {
|
||||||
id: 'region.5127278006398860',
|
id: 'u47',
|
||||||
name: 'Hamburg',
|
name: 'John Doughnut',
|
||||||
|
}
|
||||||
|
const Paris = await Factory.build('location', {
|
||||||
|
id: 'region.9397217726497330',
|
||||||
|
name: 'Paris',
|
||||||
type: 'region',
|
type: 'region',
|
||||||
lng: 10.0,
|
lng: 2.35183,
|
||||||
lat: 53.55,
|
lat: 48.85658,
|
||||||
nameEN: 'Hamburg',
|
nameEN: 'Paris',
|
||||||
nameDE: 'Hamburg',
|
|
||||||
nameIT: 'Amburgo',
|
|
||||||
nameRU: 'Гамбург',
|
|
||||||
nameFR: 'Hambourg',
|
|
||||||
nameES: 'Hamburgo',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = await Factory.build('user', {
|
const user = await Factory.build('user', {
|
||||||
id: 'u47',
|
id: 'u47',
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
})
|
})
|
||||||
await user.relateTo(Hamburg, 'isIn')
|
await user.relateTo(Paris, 'isIn')
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await user.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the name in the requested language', async () => {
|
it('returns `null` if location translation is not available', async () => {
|
||||||
await expect(
|
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
|
||||||
query({ query: UserLocationName, variables: { id: 'u47', lang: 'RU' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
data: {
|
||||||
User: [
|
UpdateUser: {
|
||||||
expect.objectContaining({
|
name: 'John Doughnut',
|
||||||
location: expect.objectContaining({ name: 'Гамбург' }),
|
location: {
|
||||||
}),
|
nameRU: null,
|
||||||
],
|
nameEN: 'Paris',
|
||||||
},
|
},
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns a different name for a different language', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: UserLocationName, variables: { id: 'u47', lang: 'IT' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
User: [
|
|
||||||
expect.objectContaining({
|
|
||||||
location: expect.objectContaining({ name: 'Amburgo' }),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns the default name when no lang is provided', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: UserLocationName, variables: { id: 'u47' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
User: [
|
|
||||||
expect.objectContaining({
|
|
||||||
location: expect.objectContaining({ name: 'Hamburg' }),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('falls back to default when the requested translation does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: UserLocationName, variables: { id: 'u47', lang: 'ZZ' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
User: [
|
|
||||||
expect.objectContaining({
|
|
||||||
location: expect.objectContaining({ name: 'Hamburg' }),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,12 +5,26 @@
|
|||||||
/* eslint-disable @typescript-eslint/return-await */
|
/* eslint-disable @typescript-eslint/return-await */
|
||||||
import { UserInputError } from '@graphql/errors'
|
import { UserInputError } from '@graphql/errors'
|
||||||
|
|
||||||
|
import Resolver from './helpers/Resolver'
|
||||||
import { queryLocations } from './users/location'
|
import { queryLocations } from './users/location'
|
||||||
|
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Location: {
|
Location: {
|
||||||
|
...Resolver('Location', {
|
||||||
|
undefinedToNull: [
|
||||||
|
'nameEN',
|
||||||
|
'nameDE',
|
||||||
|
'nameFR',
|
||||||
|
'nameNL',
|
||||||
|
'nameIT',
|
||||||
|
'nameES',
|
||||||
|
'namePT',
|
||||||
|
'namePL',
|
||||||
|
'nameRU',
|
||||||
|
],
|
||||||
|
}),
|
||||||
distanceToMe: async (parent, _params, context: Context, _resolveInfo) => {
|
distanceToMe: async (parent, _params, context: Context, _resolveInfo) => {
|
||||||
if (!parent.id) {
|
if (!parent.id) {
|
||||||
throw new Error('Can not identify selected Location!')
|
throw new Error('Can not identify selected Location!')
|
||||||
|
|||||||
@ -12,13 +12,12 @@ import { Upload } from 'graphql-upload/public/index'
|
|||||||
import pubsubContext from '@context/pubsub'
|
import pubsubContext from '@context/pubsub'
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
||||||
|
import CreateRoom from '@graphql/queries/messaging/CreateRoom.gql'
|
||||||
import MarkMessagesAsSeen from '@graphql/queries/messaging/MarkMessagesAsSeen.gql'
|
import MarkMessagesAsSeen from '@graphql/queries/messaging/MarkMessagesAsSeen.gql'
|
||||||
import Message from '@graphql/queries/messaging/Message.gql'
|
import Message from '@graphql/queries/messaging/Message.gql'
|
||||||
import Room from '@graphql/queries/messaging/Room.gql'
|
import Room from '@graphql/queries/messaging/Room.gql'
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
|
||||||
import { chatMessageAddedFilter, chatMessageStatusUpdatedFilter } from './messages'
|
|
||||||
|
|
||||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
@ -126,14 +125,13 @@ describe('Message', () => {
|
|||||||
describe('room exists', () => {
|
describe('room exists', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
const result = await mutate({
|
const room = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'other-chatting-user',
|
userId: 'other-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
roomId = result.data.CreateMessage.room.id
|
roomId = room.data.CreateRoom.id
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user chats in room', () => {
|
describe('user chats in room', () => {
|
||||||
@ -204,7 +202,7 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('unread count for other user', () => {
|
describe('unread count for other user', () => {
|
||||||
it('has unread count = 2', async () => {
|
it('has unread count = 1', async () => {
|
||||||
authenticatedUser = await otherChattingUser.toJson()
|
authenticatedUser = await otherChattingUser.toJson()
|
||||||
await expect(query({ query: Room })).resolves.toMatchObject({
|
await expect(query({ query: Room })).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -212,7 +210,7 @@ describe('Message', () => {
|
|||||||
Room: [
|
Room: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
lastMessageAt: expect.any(String),
|
lastMessageAt: expect.any(String),
|
||||||
unreadCount: 2,
|
unreadCount: 1,
|
||||||
lastMessage: expect.objectContaining({
|
lastMessage: expect.objectContaining({
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
@ -331,7 +329,7 @@ describe('Message', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Message: [expect.objectContaining({ content: 'init' })],
|
Message: [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -409,14 +407,13 @@ describe('Message', () => {
|
|||||||
describe('room exists with authenticated user chatting', () => {
|
describe('room exists with authenticated user chatting', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
const result = await mutate({
|
const room = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'other-chatting-user',
|
userId: 'other-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
roomId = result.data.CreateMessage.room.id
|
roomId = room.data.CreateRoom.id
|
||||||
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
@ -438,15 +435,10 @@ describe('Message', () => {
|
|||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Message: [
|
Message: [
|
||||||
expect.objectContaining({
|
|
||||||
indexId: 0,
|
|
||||||
content: 'init',
|
|
||||||
senderId: 'chatting-user',
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
_id: result.data?.Message[1].id,
|
_id: result.data?.Message[0].id,
|
||||||
indexId: 1,
|
indexId: 0,
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
@ -493,14 +485,9 @@ describe('Message', () => {
|
|||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Message: [
|
Message: [
|
||||||
expect.objectContaining({
|
|
||||||
indexId: 0,
|
|
||||||
content: 'init',
|
|
||||||
senderId: 'chatting-user',
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
indexId: 1,
|
indexId: 0,
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
@ -512,7 +499,7 @@ describe('Message', () => {
|
|||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
indexId: 2,
|
indexId: 1,
|
||||||
content: 'A nice response message to chatting user',
|
content: 'A nice response message to chatting user',
|
||||||
senderId: 'other-chatting-user',
|
senderId: 'other-chatting-user',
|
||||||
username: 'Other Chatting User',
|
username: 'Other Chatting User',
|
||||||
@ -524,7 +511,7 @@ describe('Message', () => {
|
|||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
indexId: 3,
|
indexId: 2,
|
||||||
content: 'And another nice message to other chatting user',
|
content: 'And another nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
username: 'Chatting User',
|
username: 'Chatting User',
|
||||||
@ -540,8 +527,6 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns the messages paginated', async () => {
|
it('returns the messages paginated', async () => {
|
||||||
// Messages ordered by indexId DESC: 3, 2, 1, 0
|
|
||||||
// first: 2, offset: 0 → indexId 2 and 3 (reversed to ASC)
|
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: Message,
|
query: Message,
|
||||||
@ -556,20 +541,27 @@ describe('Message', () => {
|
|||||||
data: {
|
data: {
|
||||||
Message: [
|
Message: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
indexId: 2,
|
id: expect.any(String),
|
||||||
|
indexId: 1,
|
||||||
content: 'A nice response message to chatting user',
|
content: 'A nice response message to chatting user',
|
||||||
senderId: 'other-chatting-user',
|
senderId: 'other-chatting-user',
|
||||||
|
username: 'Other Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
indexId: 3,
|
id: expect.any(String),
|
||||||
|
indexId: 2,
|
||||||
content: 'And another nice message to other chatting user',
|
content: 'And another nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// first: 2, offset: 2 → indexId 0 and 1 (reversed to ASC)
|
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: Message,
|
query: Message,
|
||||||
@ -584,14 +576,13 @@ describe('Message', () => {
|
|||||||
data: {
|
data: {
|
||||||
Message: [
|
Message: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
indexId: 0,
|
indexId: 0,
|
||||||
content: 'init',
|
|
||||||
senderId: 'chatting-user',
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
indexId: 1,
|
|
||||||
content: 'Some nice message to other chatting user',
|
content: 'Some nice message to other chatting user',
|
||||||
senderId: 'chatting-user',
|
senderId: 'chatting-user',
|
||||||
|
username: 'Chatting User',
|
||||||
|
avatar: expect.any(String),
|
||||||
|
date: expect.any(String),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -648,14 +639,13 @@ describe('Message', () => {
|
|||||||
const messageIds: string[] = []
|
const messageIds: string[] = []
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
const result = await mutate({
|
const room = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'other-chatting-user',
|
userId: 'other-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
roomId = result.data.CreateMessage.room.id
|
roomId = room.data.CreateRoom.id
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
@ -722,7 +712,6 @@ describe('Message', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
Message: [
|
Message: [
|
||||||
expect.objectContaining({ seen: true }),
|
|
||||||
expect.objectContaining({ seen: true }),
|
expect.objectContaining({ seen: true }),
|
||||||
expect.objectContaining({ seen: false }),
|
expect.objectContaining({ seen: false }),
|
||||||
expect.objectContaining({ seen: true }),
|
expect.objectContaining({ seen: true }),
|
||||||
@ -732,124 +721,4 @@ describe('Message', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('message query with beforeIndex', () => {
|
|
||||||
let testRoomId: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
authenticatedUser = await chattingUser.toJson()
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { userId: 'other-chatting-user', content: 'msg-0' },
|
|
||||||
})
|
|
||||||
testRoomId = result.data.CreateMessage.room.id
|
|
||||||
await mutate({ mutation: CreateMessage, variables: { roomId: testRoomId, content: 'msg-1' } })
|
|
||||||
await mutate({ mutation: CreateMessage, variables: { roomId: testRoomId, content: 'msg-2' } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns only messages with indexId less than beforeIndex', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: Message,
|
|
||||||
variables: { roomId: testRoomId, beforeIndex: 2 },
|
|
||||||
})
|
|
||||||
expect(result.errors).toBeUndefined()
|
|
||||||
const indexIds: number[] = result.data.Message.map((m: { indexId: number }) => m.indexId)
|
|
||||||
expect(indexIds.every((id: number) => id < 2)).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('subscription filters', () => {
|
|
||||||
describe('chatMessageAddedFilter', () => {
|
|
||||||
it('returns true for recipient and marks as distributed', async () => {
|
|
||||||
const mockSession = {
|
|
||||||
writeTransaction: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue([{ roomId: 'r1', authorId: 'a1', messageIds: ['m1'] }]),
|
|
||||||
close: jest.fn(),
|
|
||||||
}
|
|
||||||
const filterContext = {
|
|
||||||
user: { id: 'recipient' },
|
|
||||||
driver: { session: () => mockSession },
|
|
||||||
pubsub: { publish: jest.fn() },
|
|
||||||
}
|
|
||||||
const result = await chatMessageAddedFilter(
|
|
||||||
{ userId: 'recipient', chatMessageAdded: { id: 'm1' } },
|
|
||||||
filterContext,
|
|
||||||
)
|
|
||||||
expect(result).toBe(true)
|
|
||||||
expect(mockSession.writeTransaction).toHaveBeenCalled()
|
|
||||||
expect(filterContext.pubsub.publish).toHaveBeenCalledWith(
|
|
||||||
'CHAT_MESSAGE_STATUS_UPDATED',
|
|
||||||
expect.objectContaining({
|
|
||||||
chatMessageStatusUpdated: { roomId: 'r1', messageIds: ['m1'], status: 'distributed' },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false for non-recipient', async () => {
|
|
||||||
const result = await chatMessageAddedFilter(
|
|
||||||
{ userId: 'other', chatMessageAdded: { id: 'm1' } },
|
|
||||||
{ user: { id: 'me' } },
|
|
||||||
)
|
|
||||||
expect(result).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('skips distributed marking when no message id', async () => {
|
|
||||||
const mockSession = { writeTransaction: jest.fn(), close: jest.fn() }
|
|
||||||
const result = await chatMessageAddedFilter(
|
|
||||||
{ userId: 'me', chatMessageAdded: {} },
|
|
||||||
{ user: { id: 'me' }, driver: { session: () => mockSession } },
|
|
||||||
)
|
|
||||||
expect(result).toBe(true)
|
|
||||||
expect(mockSession.writeTransaction).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('chatMessageStatusUpdatedFilter', () => {
|
|
||||||
it('returns true when authorId matches', () => {
|
|
||||||
expect(chatMessageStatusUpdatedFilter({ authorId: 'u1' }, { user: { id: 'u1' } })).toBe(
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false when authorId does not match', () => {
|
|
||||||
expect(chatMessageStatusUpdatedFilter({ authorId: 'u1' }, { user: { id: 'u2' } })).toBe(
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('create message validation', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
authenticatedUser = await chattingUser.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects creating a room with self', async () => {
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { userId: 'chatting-user', content: 'test' },
|
|
||||||
})
|
|
||||||
expect(result.errors).toBeDefined()
|
|
||||||
expect(result.errors?.[0].message).toContain('Cannot create a room with self')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects missing roomId and userId', async () => {
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { content: 'test' },
|
|
||||||
})
|
|
||||||
expect(result.errors).toBeDefined()
|
|
||||||
expect(result.errors?.[0].message).toContain('Either roomId or userId must be provided')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects empty content without files', async () => {
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateMessage,
|
|
||||||
variables: { userId: 'other-chatting-user', content: '' },
|
|
||||||
})
|
|
||||||
expect(result.errors).toBeDefined()
|
|
||||||
expect(result.errors?.[0].message).toContain('Message must have content or files')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { withFilter } from 'graphql-subscriptions'
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
|
||||||
import CONFIG from '@config/index'
|
import CONFIG from '@config/index'
|
||||||
import { CHAT_MESSAGE_ADDED, CHAT_MESSAGE_STATUS_UPDATED } from '@constants/subscriptions'
|
import { CHAT_MESSAGE_ADDED } from '@constants/subscriptions'
|
||||||
|
|
||||||
import { attachments } from './attachments/attachments'
|
import { attachments } from './attachments/attachments'
|
||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
@ -21,65 +21,31 @@ const setMessagesAsDistributed = async (undistributedMessagesIds, session) => {
|
|||||||
const setDistributedCypher = `
|
const setDistributedCypher = `
|
||||||
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
|
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
|
||||||
SET m.distributed = true
|
SET m.distributed = true
|
||||||
WITH m
|
RETURN m { .* }
|
||||||
MATCH (m)-[:INSIDE]->(room:Room)
|
|
||||||
MATCH (m)<-[:CREATED]-(author:User)
|
|
||||||
RETURN DISTINCT room.id AS roomId, author.id AS authorId, collect(m.id) AS messageIds
|
|
||||||
`
|
`
|
||||||
const result = await transaction.run(setDistributedCypher, {
|
const setDistributedTxResponse = await transaction.run(setDistributedCypher, {
|
||||||
undistributedMessagesIds,
|
undistributedMessagesIds,
|
||||||
})
|
})
|
||||||
return result.records.map((record) => ({
|
const messages = await setDistributedTxResponse.records.map((record) => record.get('m'))
|
||||||
roomId: record.get('roomId'),
|
return messages
|
||||||
authorId: record.get('authorId'),
|
|
||||||
messageIds: record.get('messageIds'),
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chatMessageAddedFilter = async (payload, context) => {
|
|
||||||
const isRecipient = payload.userId === context.user?.id
|
|
||||||
if (isRecipient && payload.chatMessageAdded?.id) {
|
|
||||||
const session = context.driver.session()
|
|
||||||
try {
|
|
||||||
const results = await setMessagesAsDistributed([payload.chatMessageAdded.id], session)
|
|
||||||
for (const { roomId, authorId, messageIds } of results) {
|
|
||||||
void context.pubsub.publish(CHAT_MESSAGE_STATUS_UPDATED, {
|
|
||||||
authorId,
|
|
||||||
chatMessageStatusUpdated: { roomId, messageIds, status: 'distributed' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isRecipient
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chatMessageStatusUpdatedFilter = (payload, context) => {
|
|
||||||
return payload.authorId === context.user?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Subscription: {
|
Subscription: {
|
||||||
chatMessageAdded: {
|
chatMessageAdded: {
|
||||||
subscribe: withFilter(
|
subscribe: withFilter(
|
||||||
(_, __, context) => context.pubsub.asyncIterator(CHAT_MESSAGE_ADDED),
|
(_, __, context) => context.pubsub.asyncIterator(CHAT_MESSAGE_ADDED),
|
||||||
async (payload, variables, context) => chatMessageAddedFilter(payload, context),
|
(payload, variables, context) => {
|
||||||
),
|
return payload.userId === context.user?.id
|
||||||
},
|
},
|
||||||
chatMessageStatusUpdated: {
|
|
||||||
subscribe: withFilter(
|
|
||||||
(_, __, context) => context.pubsub.asyncIterator(CHAT_MESSAGE_STATUS_UPDATED),
|
|
||||||
(payload, variables, context) => chatMessageStatusUpdatedFilter(payload, context),
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
Message: async (object, params, context, resolveInfo) => {
|
Message: async (object, params, context, resolveInfo) => {
|
||||||
const { roomId, beforeIndex } = params
|
const { roomId } = params
|
||||||
delete params.roomId
|
delete params.roomId
|
||||||
delete params.beforeIndex
|
|
||||||
if (!params.filter) params.filter = {}
|
if (!params.filter) params.filter = {}
|
||||||
params.filter.room = {
|
params.filter.room = {
|
||||||
id: roomId,
|
id: roomId,
|
||||||
@ -87,128 +53,73 @@ export default {
|
|||||||
id: context.user.id,
|
id: context.user.id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (beforeIndex !== undefined && beforeIndex !== null) {
|
|
||||||
params.filter.indexId_lt = beforeIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
||||||
|
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
// Mark undistributed messages as distributed (fallback for missed socket deliveries)
|
|
||||||
const undistributedMessagesIds = resolved
|
const undistributedMessagesIds = resolved
|
||||||
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
|
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
|
||||||
.map((msg) => msg.id)
|
.map((msg) => msg.id)
|
||||||
if (undistributedMessagesIds.length > 0) {
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
try {
|
try {
|
||||||
const results = await setMessagesAsDistributed(undistributedMessagesIds, session)
|
if (undistributedMessagesIds.length > 0) {
|
||||||
for (const { roomId: msgRoomId, authorId, messageIds } of results) {
|
await setMessagesAsDistributed(undistributedMessagesIds, session)
|
||||||
void context.pubsub.publish(CHAT_MESSAGE_STATUS_UPDATED, {
|
|
||||||
authorId,
|
|
||||||
chatMessageStatusUpdated: { roomId: msgRoomId, messageIds, status: 'distributed' },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await session.close()
|
await session.close()
|
||||||
}
|
}
|
||||||
|
// send subscription to author to updated the messages
|
||||||
}
|
}
|
||||||
}
|
return resolved.reverse()
|
||||||
return (resolved || []).reverse()
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateMessage: async (_parent, params, context, _resolveInfo) => {
|
CreateMessage: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { roomId, userId, content, files = [] } = params
|
const { roomId, content, files = [] } = params
|
||||||
const {
|
const {
|
||||||
user: { id: currentUserId },
|
user: { id: currentUserId },
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
if (userId && userId === currentUserId) {
|
|
||||||
throw new Error('Cannot create a room with self')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!roomId && !userId) {
|
|
||||||
throw new Error('Either roomId or userId must be provided')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content?.trim() && files.length === 0) {
|
|
||||||
throw new Error('Message must have content or files')
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await session.writeTransaction(async (transaction) => {
|
return await session.writeTransaction(async (transaction) => {
|
||||||
// If userId is provided, find-or-create a DM room first
|
|
||||||
if (userId) {
|
|
||||||
await transaction.run(
|
|
||||||
`
|
|
||||||
MATCH (currentUser:User { id: $currentUserId })
|
|
||||||
MATCH (user:User { id: $userId })
|
|
||||||
OPTIONAL MATCH (currentUser)-[:CHATS_IN]->(existingRoom:Room)<-[:CHATS_IN]-(user)
|
|
||||||
WHERE NOT (existingRoom)-[:ROOM_FOR]->(:Group)
|
|
||||||
WITH currentUser, user, collect(existingRoom)[0] AS existingRoom
|
|
||||||
WITH currentUser, user, existingRoom
|
|
||||||
WHERE existingRoom IS NULL
|
|
||||||
CREATE (currentUser)-[:CHATS_IN]->(:Room {
|
|
||||||
createdAt: toString(datetime()),
|
|
||||||
id: apoc.create.uuid()
|
|
||||||
})<-[:CHATS_IN]-(user)
|
|
||||||
`,
|
|
||||||
{ currentUserId, userId },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the room — either by roomId or by finding the DM room with userId
|
|
||||||
const matchRoom = roomId
|
|
||||||
? `MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })`
|
|
||||||
: `MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user:User { id: $userId })
|
|
||||||
WHERE NOT (room)-[:ROOM_FOR]->(:Group)`
|
|
||||||
|
|
||||||
const createMessageCypher = `
|
const createMessageCypher = `
|
||||||
${matchRoom}
|
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
||||||
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
|
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
|
||||||
OPTIONAL MATCH (existing:Message)-[:INSIDE]->(room)
|
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)
|
||||||
WITH room, currentUser, image, MAX(existing.indexId) AS maxIndex
|
OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User)
|
||||||
SET room.messageCounter = CASE
|
WHERE NOT recipientUser.id = $currentUserId
|
||||||
WHEN room.messageCounter IS NOT NULL THEN room.messageCounter + 1
|
WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser
|
||||||
WHEN maxIndex IS NOT NULL THEN maxIndex + 2
|
|
||||||
ELSE 1
|
|
||||||
END,
|
|
||||||
room.lastMessageAt = toString(datetime())
|
|
||||||
WITH room, currentUser, image
|
|
||||||
CREATE (currentUser)-[:CREATED]->(message:Message {
|
CREATE (currentUser)-[:CREATED]->(message:Message {
|
||||||
createdAt: toString(datetime()),
|
createdAt: toString(datetime()),
|
||||||
id: apoc.create.uuid(),
|
id: apoc.create.uuid(),
|
||||||
indexId: room.messageCounter - 1,
|
indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END,
|
||||||
content: LEFT($content,2000),
|
content: LEFT($content,2000),
|
||||||
saved: true,
|
saved: true,
|
||||||
distributed: false
|
distributed: false,
|
||||||
|
seen: false
|
||||||
})-[:INSIDE]->(room)
|
})-[:INSIDE]->(room)
|
||||||
WITH message, currentUser, image, room
|
SET room.lastMessageAt = toString(datetime())
|
||||||
OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipient:User)
|
|
||||||
WHERE NOT recipient.id = $currentUserId
|
|
||||||
WITH message, currentUser, image, collect(recipient) AS recipients
|
|
||||||
FOREACH (r IN recipients | CREATE (r)-[:HAS_NOT_SEEN]->(message))
|
|
||||||
RETURN message {
|
RETURN message {
|
||||||
.*,
|
.*,
|
||||||
indexId: toString(message.indexId),
|
indexId: toString(message.indexId),
|
||||||
|
recipientId: recipientUser.id,
|
||||||
senderId: currentUser.id,
|
senderId: currentUser.id,
|
||||||
username: currentUser.name,
|
username: currentUser.name,
|
||||||
avatar: image.url,
|
avatar: image.url,
|
||||||
date: message.createdAt,
|
date: message.createdAt
|
||||||
seen: false
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const txResponse = await transaction.run(createMessageCypher, {
|
const createMessageTxResponse = await transaction.run(createMessageCypher, {
|
||||||
currentUserId,
|
currentUserId,
|
||||||
roomId,
|
roomId,
|
||||||
userId,
|
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [message] = txResponse.records.map((record) => record.get('message'))
|
const [message] = createMessageTxResponse.records.map((record) => record.get('message'))
|
||||||
|
|
||||||
|
// this is the case if the room doesn't exist - requires refactoring for implicit rooms
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -239,30 +150,20 @@ export default {
|
|||||||
const currentUserId = context.user.id
|
const currentUserId = context.user.id
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
try {
|
try {
|
||||||
const result = await session.writeTransaction(async (transaction) => {
|
await session.writeTransaction(async (transaction) => {
|
||||||
const cypher = `
|
const setSeenCypher = `
|
||||||
MATCH (user:User { id: $currentUserId })-[r:HAS_NOT_SEEN]->(m:Message)
|
MATCH (m:Message)<-[:CREATED]-(user:User)
|
||||||
WHERE m.id IN $messageIds
|
WHERE m.id IN $messageIds AND NOT user.id = $currentUserId
|
||||||
DELETE r
|
SET m.seen = true
|
||||||
WITH m
|
RETURN m { .* }
|
||||||
MATCH (m)-[:INSIDE]->(room:Room)
|
|
||||||
MATCH (m)<-[:CREATED]-(author:User)
|
|
||||||
RETURN DISTINCT room.id AS roomId, author.id AS authorId
|
|
||||||
`
|
`
|
||||||
return transaction.run(cypher, {
|
const setSeenTxResponse = await transaction.run(setSeenCypher, {
|
||||||
messageIds,
|
messageIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
})
|
})
|
||||||
|
return setSeenTxResponse.records.map((record) => record.get('m'))
|
||||||
})
|
})
|
||||||
// Notify message authors that their messages have been seen
|
// send subscription to author to updated the messages
|
||||||
for (const record of result.records) {
|
|
||||||
const roomId = record.get('roomId')
|
|
||||||
const authorId = record.get('authorId')
|
|
||||||
void context.pubsub.publish(CHAT_MESSAGE_STATUS_UPDATED, {
|
|
||||||
authorId,
|
|
||||||
chatMessageStatusUpdated: { roomId, messageIds, status: 'seen' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
} finally {
|
} finally {
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|||||||
@ -293,7 +293,7 @@ describe('pin groupPosts', () => {
|
|||||||
config = { ...defaultConfig, MAX_GROUP_PINNED_POSTS: 2 }
|
config = { ...defaultConfig, MAX_GROUP_PINNED_POSTS: 2 }
|
||||||
authenticatedUser = await publicUser.toJson()
|
authenticatedUser = await publicUser.toJson()
|
||||||
})
|
})
|
||||||
it('returns pinned posts before unpinned posts', async () => {
|
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-1-to-public-group' } })
|
||||||
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
await mutate({ mutation: pinGroupPost, variables: { id: 'post-2-to-public-group' } })
|
||||||
await expect(
|
await expect(
|
||||||
@ -308,9 +308,8 @@ describe('pin groupPosts', () => {
|
|||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
profilePagePosts: [
|
profilePagePosts: [
|
||||||
// Order between pinned posts may vary (same sortDate), so use arrayContaining for the first two
|
expect.objectContaining({ id: 'post-2-to-public-group', groupPinned: true }),
|
||||||
expect.objectContaining({ groupPinned: true }),
|
expect.objectContaining({ id: 'post-1-to-public-group', groupPinned: true }),
|
||||||
expect.objectContaining({ groupPinned: true }),
|
|
||||||
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: null }),
|
expect.objectContaining({ id: 'post-3-to-public-group', groupPinned: null }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2000,6 +2000,7 @@ describe('DeletePost', () => {
|
|||||||
id: 'p4711',
|
id: 'p4711',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
image: null,
|
image: null,
|
||||||
comments: [],
|
comments: [],
|
||||||
},
|
},
|
||||||
@ -2014,6 +2015,7 @@ describe('DeletePost', () => {
|
|||||||
'comment',
|
'comment',
|
||||||
{
|
{
|
||||||
content: 'to be deleted comment content',
|
content: 'to be deleted comment content',
|
||||||
|
contentExcerpt: 'to be deleted comment content',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
postId: 'p4711',
|
postId: 'p4711',
|
||||||
@ -2028,12 +2030,14 @@ describe('DeletePost', () => {
|
|||||||
id: 'p4711',
|
id: 'p4711',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
image: null,
|
image: null,
|
||||||
comments: [
|
comments: [
|
||||||
{
|
{
|
||||||
deleted: true,
|
deleted: true,
|
||||||
// Should we black out the comment content in the database, too?
|
// Should we black out the comment content in the database, too?
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { UserInputError } from '@graphql/errors'
|
|||||||
|
|
||||||
import { validateEventParams } from './helpers/events'
|
import { validateEventParams } from './helpers/events'
|
||||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||||
import { filterPostsHasLocation } from './helpers/filterHasLocation'
|
|
||||||
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
||||||
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
|
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
|
||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
@ -62,7 +61,6 @@ export default {
|
|||||||
params = await filterInvisiblePosts(params, context)
|
params = await filterInvisiblePosts(params, context)
|
||||||
params = await filterForMutedUsers(params, context)
|
params = await filterForMutedUsers(params, context)
|
||||||
params = filterEventDates(params)
|
params = filterEventDates(params)
|
||||||
params = await filterPostsHasLocation(params, context)
|
|
||||||
params = await maintainPinnedPosts(params)
|
params = await maintainPinnedPosts(params)
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
},
|
},
|
||||||
@ -70,7 +68,6 @@ 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 filterPostsHasLocation(params, context)
|
|
||||||
params = await maintainGroupPinnedPosts(params)
|
params = await maintainGroupPinnedPosts(params)
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
},
|
},
|
||||||
@ -299,6 +296,7 @@ export default {
|
|||||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||||
SET post.deleted = TRUE
|
SET post.deleted = TRUE
|
||||||
SET post.content = 'UNAVAILABLE'
|
SET post.content = 'UNAVAILABLE'
|
||||||
|
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||||
SET post.title = 'UNAVAILABLE'
|
SET post.title = 'UNAVAILABLE'
|
||||||
SET comment.deleted = TRUE
|
SET comment.deleted = TRUE
|
||||||
RETURN post {.*}
|
RETURN post {.*}
|
||||||
|
|||||||
@ -3,14 +3,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
import CreateGroupRoom from '@graphql/queries/messaging/CreateGroupRoom.gql'
|
|
||||||
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
||||||
|
import CreateRoom from '@graphql/queries/messaging/CreateRoom.gql'
|
||||||
import Room from '@graphql/queries/messaging/Room.gql'
|
import Room from '@graphql/queries/messaging/Room.gql'
|
||||||
import UnreadRooms from '@graphql/queries/messaging/UnreadRooms.gql'
|
import UnreadRooms from '@graphql/queries/messaging/UnreadRooms.gql'
|
||||||
import { createApolloTestSetup } from '@root/test/helpers'
|
import { createApolloTestSetup } from '@root/test/helpers'
|
||||||
|
|
||||||
import { roomCountUpdatedFilter } from './rooms'
|
|
||||||
|
|
||||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
@ -66,7 +64,7 @@ describe('Room', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('create room via CreateMessage with userId', () => {
|
describe('create room', () => {
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
@ -75,10 +73,9 @@ describe('Room', () => {
|
|||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'some-id',
|
userId: 'some-id',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -96,16 +93,15 @@ describe('Room', () => {
|
|||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'not-existing-user',
|
userId: 'not-existing-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
CreateMessage: null,
|
CreateRoom: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -115,10 +111,9 @@ describe('Room', () => {
|
|||||||
it('throws error', async () => {
|
it('throws error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'chatting-user',
|
userId: 'chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -128,48 +123,62 @@ describe('Room', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('user id exists', () => {
|
describe('user id exists', () => {
|
||||||
it('creates a room and returns the message with room id', async () => {
|
it('returns the id of the room', async () => {
|
||||||
const result = await mutate({
|
const result = await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'other-chatting-user',
|
userId: 'other-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
roomId = result.data.CreateMessage.room.id
|
roomId = result.data.CreateRoom.id
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
CreateMessage: {
|
CreateRoom: {
|
||||||
id: expect.any(String),
|
|
||||||
content: 'init',
|
|
||||||
room: {
|
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
roomId: result.data.CreateRoom.id,
|
||||||
|
roomName: 'Other Chatting User',
|
||||||
|
unreadCount: 0,
|
||||||
|
users: expect.arrayContaining([
|
||||||
|
{
|
||||||
|
_id: 'chatting-user',
|
||||||
|
id: 'chatting-user',
|
||||||
|
name: 'Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
_id: 'other-chatting-user',
|
||||||
|
id: 'other-chatting-user',
|
||||||
|
name: 'Other Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('send message to same user id again', () => {
|
describe('create room with same user id', () => {
|
||||||
it('returns the same room id', async () => {
|
it('returns the id of the room', async () => {
|
||||||
const result = await mutate({
|
await expect(
|
||||||
mutation: CreateMessage,
|
mutate({
|
||||||
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'other-chatting-user',
|
userId: 'other-chatting-user',
|
||||||
content: 'another message',
|
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
expect(result).toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
CreateMessage: {
|
CreateRoom: {
|
||||||
room: {
|
|
||||||
id: roomId,
|
id: roomId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -245,7 +254,7 @@ describe('Room', () => {
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
roomId: result.data.Room[0].id,
|
roomId: result.data.Room[0].id,
|
||||||
roomName: 'Chatting User',
|
roomName: 'Chatting User',
|
||||||
unreadCount: 2,
|
unreadCount: 0,
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
_id: 'chatting-user',
|
_id: 'chatting-user',
|
||||||
@ -303,12 +312,21 @@ describe('Room', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
|
let otherRoomId: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
|
const result = await mutate({
|
||||||
|
mutation: CreateRoom,
|
||||||
|
variables: {
|
||||||
|
userId: 'not-chatting-user',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
otherRoomId = result.data.CreateRoom.roomId
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'not-chatting-user',
|
roomId: otherRoomId,
|
||||||
content: 'Message to not chatting user',
|
content: 'Message to not chatting user',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -327,10 +345,17 @@ describe('Room', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
authenticatedUser = await otherChattingUser.toJson()
|
authenticatedUser = await otherChattingUser.toJson()
|
||||||
|
const result2 = await mutate({
|
||||||
|
mutation: CreateRoom,
|
||||||
|
variables: {
|
||||||
|
userId: 'not-chatting-user',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
otherRoomId = result2.data.CreateRoom.roomId
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateMessage,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'not-chatting-user',
|
roomId: otherRoomId,
|
||||||
content: 'Other message to not chatting user',
|
content: 'Other message to not chatting user',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -415,23 +440,23 @@ describe('Room', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
authenticatedUser = await chattingUser.toJson()
|
authenticatedUser = await chattingUser.toJson()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'second-chatting-user',
|
userId: 'second-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: CreateMessage,
|
mutation: CreateRoom,
|
||||||
variables: {
|
variables: {
|
||||||
userId: 'third-chatting-user',
|
userId: 'third-chatting-user',
|
||||||
content: 'init',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the rooms paginated', async () => {
|
it('returns the rooms paginated', async () => {
|
||||||
await expect(query({ query: Room, variables: { first: 3 } })).resolves.toMatchObject({
|
await expect(
|
||||||
|
query({ query: Room, variables: { first: 3, offset: 0 } }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
data: {
|
data: {
|
||||||
Room: expect.arrayContaining([
|
Room: expect.arrayContaining([
|
||||||
@ -439,12 +464,9 @@ describe('Room', () => {
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
roomId: expect.any(String),
|
roomId: expect.any(String),
|
||||||
roomName: 'Third Chatting User',
|
roomName: 'Third Chatting User',
|
||||||
lastMessageAt: expect.any(String),
|
lastMessageAt: null,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
lastMessage: expect.objectContaining({
|
lastMessage: null,
|
||||||
content: 'init',
|
|
||||||
senderId: 'chatting-user',
|
|
||||||
}),
|
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
_id: 'chatting-user',
|
_id: 'chatting-user',
|
||||||
@ -468,12 +490,9 @@ describe('Room', () => {
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
roomId: expect.any(String),
|
roomId: expect.any(String),
|
||||||
roomName: 'Second Chatting User',
|
roomName: 'Second Chatting User',
|
||||||
lastMessageAt: expect.any(String),
|
lastMessageAt: null,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
lastMessage: expect.objectContaining({
|
lastMessage: null,
|
||||||
content: 'init',
|
|
||||||
senderId: 'chatting-user',
|
|
||||||
}),
|
|
||||||
users: expect.arrayContaining([
|
users: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
_id: 'chatting-user',
|
_id: 'chatting-user',
|
||||||
@ -533,7 +552,38 @@ describe('Room', () => {
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// Note: offset-based pagination removed in favor of cursor-based (before parameter)
|
await expect(
|
||||||
|
query({ query: Room, variables: { first: 3, offset: 3 } }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: undefined,
|
||||||
|
data: {
|
||||||
|
Room: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
roomId: expect.any(String),
|
||||||
|
roomName: 'Not Chatting User',
|
||||||
|
users: expect.arrayContaining([
|
||||||
|
{
|
||||||
|
_id: 'chatting-user',
|
||||||
|
id: 'chatting-user',
|
||||||
|
name: 'Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'not-chatting-user',
|
||||||
|
id: 'not-chatting-user',
|
||||||
|
name: 'Not Chatting User',
|
||||||
|
avatar: {
|
||||||
|
url: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -589,160 +639,4 @@ describe('Room', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('query room by userId', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
authenticatedUser = await chattingUser.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns the DM room with the specified user', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: Room,
|
|
||||||
variables: { userId: 'other-chatting-user' },
|
|
||||||
})
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
errors: undefined,
|
|
||||||
data: {
|
|
||||||
Room: [
|
|
||||||
expect.objectContaining({
|
|
||||||
roomName: 'Other Chatting User',
|
|
||||||
users: expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: 'chatting-user' }),
|
|
||||||
expect.objectContaining({ id: 'other-chatting-user' }),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns empty when no DM room exists', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: Room,
|
|
||||||
variables: { userId: 'non-existent-user' },
|
|
||||||
})
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
errors: undefined,
|
|
||||||
data: {
|
|
||||||
Room: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('query room by groupId', () => {
|
|
||||||
let groupRoomId: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await Factory.build(
|
|
||||||
'group',
|
|
||||||
{
|
|
||||||
id: 'test-group',
|
|
||||||
name: 'Test Group',
|
|
||||||
},
|
|
||||||
{ owner: chattingUser },
|
|
||||||
)
|
|
||||||
// Add other user as member
|
|
||||||
const session = database.driver.session()
|
|
||||||
try {
|
|
||||||
await session.writeTransaction((txc) =>
|
|
||||||
txc.run(
|
|
||||||
`MATCH (u:User {id: 'other-chatting-user'}), (g:Group {id: 'test-group'})
|
|
||||||
MERGE (u)-[m:MEMBER_OF]->(g) SET m.role = 'usual', m.createdAt = toString(datetime())`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
authenticatedUser = await chattingUser.toJson()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CreateGroupRoom', () => {
|
|
||||||
it('creates a group room', async () => {
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateGroupRoom,
|
|
||||||
variables: { groupId: 'test-group' },
|
|
||||||
})
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
errors: undefined,
|
|
||||||
data: {
|
|
||||||
CreateGroupRoom: expect.objectContaining({
|
|
||||||
roomName: 'Test Group',
|
|
||||||
isGroupRoom: true,
|
|
||||||
users: expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: 'chatting-user' }),
|
|
||||||
expect.objectContaining({ id: 'other-chatting-user' }),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
groupRoomId = result.data.CreateGroupRoom.id
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns existing room on second call', async () => {
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateGroupRoom,
|
|
||||||
variables: { groupId: 'test-group' },
|
|
||||||
})
|
|
||||||
expect(result.data.CreateGroupRoom.id).toBe(groupRoomId)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fails for non-member', async () => {
|
|
||||||
authenticatedUser = await notChattingUser.toJson()
|
|
||||||
const result = await mutate({
|
|
||||||
mutation: CreateGroupRoom,
|
|
||||||
variables: { groupId: 'test-group' },
|
|
||||||
})
|
|
||||||
expect(result.errors).toBeDefined()
|
|
||||||
authenticatedUser = await chattingUser.toJson()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('query by groupId', () => {
|
|
||||||
it('returns the group room', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: Room,
|
|
||||||
variables: { groupId: 'test-group' },
|
|
||||||
})
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
errors: undefined,
|
|
||||||
data: {
|
|
||||||
Room: [
|
|
||||||
expect.objectContaining({
|
|
||||||
id: groupRoomId,
|
|
||||||
roomName: 'Test Group',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns empty for non-existent group', async () => {
|
|
||||||
const result = await query({
|
|
||||||
query: Room,
|
|
||||||
variables: { groupId: 'non-existent' },
|
|
||||||
})
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
errors: undefined,
|
|
||||||
data: {
|
|
||||||
Room: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('roomCountUpdatedFilter', () => {
|
|
||||||
it('returns true when payload userId matches context user', () => {
|
|
||||||
expect(roomCountUpdatedFilter({ userId: 'u1' }, {}, { user: { id: 'u1' } })).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false when userId does not match', () => {
|
|
||||||
expect(roomCountUpdatedFilter({ userId: 'u1' }, {}, { user: { id: 'u2' } })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false when context user is null', () => {
|
|
||||||
expect(roomCountUpdatedFilter({ userId: 'u1' }, {}, { user: null })).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,11 +15,10 @@ import Resolver from './helpers/Resolver'
|
|||||||
export const getUnreadRoomsCount = async (userId, session) => {
|
export const getUnreadRoomsCount = async (userId, session) => {
|
||||||
return session.readTransaction(async (transaction) => {
|
return session.readTransaction(async (transaction) => {
|
||||||
const unreadRoomsCypher = `
|
const unreadRoomsCypher = `
|
||||||
MATCH (user:User { id: $userId })-[:HAS_NOT_SEEN]->(message:Message)-[:INSIDE]->(room:Room)<-[:CHATS_IN]-(user)
|
MATCH (user:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
|
||||||
OPTIONAL MATCH (message)<-[:CREATED]-(sender:User)
|
WHERE NOT sender.id = $userId AND NOT message.seen
|
||||||
WHERE (user)-[:BLOCKED]->(sender) OR (user)-[:MUTED]->(sender)
|
AND NOT (user)-[:BLOCKED]->(sender)
|
||||||
WITH room, message, sender
|
AND NOT (user)-[:MUTED]->(sender)
|
||||||
WHERE sender IS NULL
|
|
||||||
RETURN toString(COUNT(DISTINCT room)) AS count
|
RETURN toString(COUNT(DISTINCT room)) AS count
|
||||||
`
|
`
|
||||||
const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId })
|
const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId })
|
||||||
@ -27,101 +26,24 @@ export const getUnreadRoomsCount = async (userId, session) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const roomCountUpdatedFilter = (payload, variables, context) => {
|
|
||||||
return payload.userId === context.user?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Subscription: {
|
Subscription: {
|
||||||
roomCountUpdated: {
|
roomCountUpdated: {
|
||||||
subscribe: withFilter(
|
subscribe: withFilter(
|
||||||
(_, __, context) => context.pubsub.asyncIterator(ROOM_COUNT_UPDATED),
|
(_, __, context) => context.pubsub.asyncIterator(ROOM_COUNT_UPDATED),
|
||||||
roomCountUpdatedFilter,
|
(payload, variables, context) => {
|
||||||
|
return payload.userId === context.user?.id
|
||||||
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
Room: async (object, params, context, resolveInfo) => {
|
Room: async (object, params, context, resolveInfo) => {
|
||||||
// Single room lookup by userId or groupId
|
|
||||||
if (params.userId || params.groupId) {
|
|
||||||
const session = context.driver.session()
|
|
||||||
try {
|
|
||||||
const cypher = params.groupId
|
|
||||||
? `
|
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)-[:ROOM_FOR]->(group:Group { id: $groupId })
|
|
||||||
RETURN room { .* } AS room
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user:User { id: $userId })
|
|
||||||
WHERE NOT (room)-[:ROOM_FOR]->(:Group)
|
|
||||||
RETURN room { .* } AS room
|
|
||||||
`
|
|
||||||
const result = await session.readTransaction(async (transaction) => {
|
|
||||||
return transaction.run(cypher, {
|
|
||||||
currentUserId: context.user.id,
|
|
||||||
userId: params.userId || null,
|
|
||||||
groupId: params.groupId || null,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const rooms = result.records.map((record) => record.get('room'))
|
|
||||||
if (rooms.length === 0) return []
|
|
||||||
// Re-query via neo4jgraphql to get all computed fields
|
|
||||||
delete params.userId
|
|
||||||
delete params.groupId
|
|
||||||
params.filter = { users_some: { id: context.user.id } }
|
|
||||||
params.id = rooms[0].id
|
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single room lookup by id
|
|
||||||
if (params.id) {
|
|
||||||
if (!params.filter) params.filter = {}
|
if (!params.filter) params.filter = {}
|
||||||
params.filter.users_some = { id: context.user.id }
|
params.filter.users_some = {
|
||||||
|
id: context.user.id,
|
||||||
|
}
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
}
|
|
||||||
|
|
||||||
// Room list with cursor-based pagination sorted by latest activity
|
|
||||||
const session = context.driver.session()
|
|
||||||
try {
|
|
||||||
const first = params.first || 10
|
|
||||||
const before = params.before || null
|
|
||||||
const result = await session.readTransaction(async (transaction) => {
|
|
||||||
const cypher = `
|
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)
|
|
||||||
WITH room, COALESCE(room.lastMessageAt, room.createdAt) AS sortDate
|
|
||||||
${before ? 'WHERE sortDate < $before' : ''}
|
|
||||||
RETURN room.id AS id
|
|
||||||
ORDER BY sortDate DESC
|
|
||||||
LIMIT toInteger($first)
|
|
||||||
`
|
|
||||||
return transaction.run(cypher, {
|
|
||||||
currentUserId: context.user.id,
|
|
||||||
first,
|
|
||||||
before,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const roomIds: string[] = result.records.map((record) => record.get('id') as string)
|
|
||||||
if (roomIds.length === 0) return []
|
|
||||||
// Batch query via neo4jgraphql with id_in filter (avoids N+1)
|
|
||||||
const roomParams = {
|
|
||||||
filter: {
|
|
||||||
id_in: roomIds,
|
|
||||||
users_some: { id: context.user.id },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const rooms = await neo4jgraphql(object, roomParams, context, resolveInfo)
|
|
||||||
// Preserve the sort order from the cursor query
|
|
||||||
const orderMap = new Map<string, number>(roomIds.map((id, i) => [id, i]))
|
|
||||||
return (rooms || []).sort(
|
|
||||||
(a: { id: string }, b: { id: string }) =>
|
|
||||||
(orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await session.close()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
UnreadRooms: async (_object, _params, context, _resolveInfo) => {
|
UnreadRooms: async (_object, _params, context, _resolveInfo) => {
|
||||||
const {
|
const {
|
||||||
@ -137,51 +59,46 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateGroupRoom: async (_parent, params, context, _resolveInfo) => {
|
CreateRoom: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { groupId } = params
|
const { userId } = params
|
||||||
const {
|
const {
|
||||||
user: { id: currentUserId },
|
user: { id: currentUserId },
|
||||||
} = context
|
} = context
|
||||||
|
if (userId === currentUserId) {
|
||||||
|
throw new Error('Cannot create a room with self')
|
||||||
|
}
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
try {
|
try {
|
||||||
const room = await session.writeTransaction(async (transaction) => {
|
const room = await session.writeTransaction(async (transaction) => {
|
||||||
// Step 1: Create/merge the room and add all active group members to it
|
const createRoomCypher = `
|
||||||
const createGroupRoomCypher = `
|
MATCH (currentUser:User { id: $currentUserId })
|
||||||
MATCH (currentUser:User { id: $currentUserId })-[membership:MEMBER_OF]->(group:Group { id: $groupId })
|
MATCH (user:User { id: $userId })
|
||||||
WHERE membership.role IN ['usual', 'admin', 'owner']
|
MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user)
|
||||||
MERGE (room:Room)-[:ROOM_FOR]->(group)
|
|
||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
room.createdAt = toString(datetime()),
|
room.createdAt = toString(datetime()),
|
||||||
room.id = apoc.create.uuid()
|
room.id = apoc.create.uuid()
|
||||||
WITH room, group, currentUser
|
WITH room, user, currentUser
|
||||||
MATCH (member:User)-[m:MEMBER_OF]->(group)
|
OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
|
||||||
WHERE m.role IN ['usual', 'admin', 'owner']
|
WHERE NOT sender.id = $currentUserId AND NOT message.seen
|
||||||
MERGE (member)-[:CHATS_IN]->(room)
|
WITH room, user, currentUser, message,
|
||||||
WITH room, group, currentUser, collect(properties(member)) AS members
|
user.name AS roomName
|
||||||
OPTIONAL MATCH (currentUser)-[:HAS_NOT_SEEN]->(message:Message)-[:INSIDE]->(room)
|
|
||||||
WITH room, group, members, COUNT(DISTINCT message) AS unread
|
|
||||||
OPTIONAL MATCH (group)-[:AVATAR_IMAGE]->(groupImg:Image)
|
|
||||||
RETURN room {
|
RETURN room {
|
||||||
.*,
|
.*,
|
||||||
roomName: group.name,
|
users: [properties(currentUser), properties(user)],
|
||||||
avatar: groupImg.url,
|
roomName: roomName,
|
||||||
isGroupRoom: true,
|
unreadCount: toString(COUNT(DISTINCT message))
|
||||||
group: properties(group),
|
|
||||||
users: members,
|
|
||||||
unreadCount: toString(unread)
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const createGroupRoomTxResponse = await transaction.run(createGroupRoomCypher, {
|
const createRoomTxResponse = await transaction.run(createRoomCypher, {
|
||||||
groupId,
|
userId,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
})
|
})
|
||||||
const [room] = createGroupRoomTxResponse.records.map((record) => record.get('room'))
|
const [room] = createRoomTxResponse.records.map((record) => record.get('room'))
|
||||||
return room
|
return room
|
||||||
})
|
})
|
||||||
if (!room) {
|
if (room) {
|
||||||
throw new Error('Could not create group room. User may not be a member of the group.')
|
|
||||||
}
|
|
||||||
room.roomId = room.id
|
room.roomId = room.id
|
||||||
|
}
|
||||||
return room
|
return room
|
||||||
} finally {
|
} finally {
|
||||||
await session.close()
|
await session.close()
|
||||||
@ -194,9 +111,6 @@ export default {
|
|||||||
hasMany: {
|
hasMany: {
|
||||||
users: '<-[:CHATS_IN]-(related:User)',
|
users: '<-[:CHATS_IN]-(related:User)',
|
||||||
},
|
},
|
||||||
hasOne: {
|
|
||||||
group: '-[:ROOM_FOR]->(related:Group)',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
import { queryString } from './searches/queryString'
|
import { queryString } from './searches/queryString'
|
||||||
|
|
||||||
@ -18,7 +17,7 @@ const cypherTemplate = (setup) => `
|
|||||||
${setup.withClause}
|
${setup.withClause}
|
||||||
RETURN
|
RETURN
|
||||||
${setup.returnClause}
|
${setup.returnClause}
|
||||||
AS result${setup.returnScore === false ? '' : ', score'}
|
AS result
|
||||||
SKIP toInteger($skip)
|
SKIP toInteger($skip)
|
||||||
${setup.limit}
|
${setup.limit}
|
||||||
`
|
`
|
||||||
@ -38,9 +37,9 @@ const searchPostsSetup = {
|
|||||||
MATCH (user:User {id: $userId})
|
MATCH (user:User {id: $userId})
|
||||||
OPTIONAL MATCH (user)-[block:MUTED]->(author)
|
OPTIONAL MATCH (user)-[block:MUTED]->(author)
|
||||||
OPTIONAL MATCH (user)-[restriction:CANNOT_SEE]->(resource)
|
OPTIONAL MATCH (user)-[restriction:CANNOT_SEE]->(resource)
|
||||||
WITH user, resource, author, block, restriction, score`,
|
WITH user, resource, author, block, restriction`,
|
||||||
whereClause: postWhereClause,
|
whereClause: postWhereClause,
|
||||||
withClause: `WITH resource, author, score,
|
withClause: `WITH resource, author,
|
||||||
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
|
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
|
||||||
[(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
|
[(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
|
||||||
returnClause: `resource {
|
returnClause: `resource {
|
||||||
@ -78,32 +77,18 @@ const searchGroupsSetup = {
|
|||||||
match: `MATCH (resource:Group)
|
match: `MATCH (resource:Group)
|
||||||
MATCH (user:User {id: $userId})
|
MATCH (user:User {id: $userId})
|
||||||
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(resource)
|
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(resource)
|
||||||
WITH user, resource, membership, score`,
|
WITH user, resource, membership`,
|
||||||
whereClause: `WHERE score >= 0.0
|
whereClause: `WHERE score >= 0.0
|
||||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
AND NOT (resource.deleted = true OR resource.disabled = true)
|
||||||
AND (resource.groupType IN ['public', 'closed']
|
AND (resource.groupType IN ['public', 'closed']
|
||||||
OR membership.role IN ['usual', 'admin', 'owner'])`,
|
OR membership.role IN ['usual', 'admin', 'owner'])`,
|
||||||
withClause: 'WITH resource, membership, score',
|
withClause: 'WITH resource, membership',
|
||||||
returnClause: `resource { .*, myRole: membership.role, __typename: 'Group' }`,
|
|
||||||
limit: 'LIMIT toInteger($limit)',
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchMyGroupsSetup = {
|
|
||||||
fulltextIndex: 'group_fulltext_search',
|
|
||||||
match: `MATCH (resource:Group)
|
|
||||||
MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(resource)
|
|
||||||
WITH user, resource, membership, score`,
|
|
||||||
whereClause: `WHERE score >= 0.0
|
|
||||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
|
||||||
AND membership.role IN ['usual', 'admin', 'owner']`,
|
|
||||||
withClause: 'WITH resource, membership, score',
|
|
||||||
returnClause: `resource { .*, myRole: membership.role, __typename: 'Group' }`,
|
returnClause: `resource { .*, myRole: membership.role, __typename: 'Group' }`,
|
||||||
limit: 'LIMIT toInteger($limit)',
|
limit: 'LIMIT toInteger($limit)',
|
||||||
}
|
}
|
||||||
|
|
||||||
const countSetup = {
|
const countSetup = {
|
||||||
returnClause: 'toString(size(collect(resource)))',
|
returnClause: 'toString(size(collect(resource)))',
|
||||||
returnScore: false,
|
|
||||||
limit: '',
|
limit: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,10 +116,7 @@ const searchResultPromise = async (session, setup, params) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchResultCallback = (result) => {
|
const searchResultCallback = (result) => {
|
||||||
const response = result.records.map((r) => ({
|
const response = result.records.map((r) => r.get('result'))
|
||||||
...r.get('result'),
|
|
||||||
_score: r.has('score') ? r.get('score') : 0,
|
|
||||||
}))
|
|
||||||
if (Array.isArray(response) && response.length && response[0].__typename === 'Post') {
|
if (Array.isArray(response) && response.length && response[0].__typename === 'Post') {
|
||||||
response.forEach((post) => {
|
response.forEach((post) => {
|
||||||
post.postType = [post.postType]
|
post.postType = [post.postType]
|
||||||
@ -250,23 +232,6 @@ export default {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
searchChatTargets: async (_parent, args, context, _resolveInfo) => {
|
|
||||||
const { query } = args
|
|
||||||
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 50))
|
|
||||||
const userId = context.user?.id || null
|
|
||||||
const params = {
|
|
||||||
query: queryString(query),
|
|
||||||
skip: 0,
|
|
||||||
limit,
|
|
||||||
userId,
|
|
||||||
}
|
|
||||||
const results = [
|
|
||||||
...(await getSearchResults(context, searchUsersSetup, params)),
|
|
||||||
...(await getSearchResults(context, searchMyGroupsSetup, params)),
|
|
||||||
]
|
|
||||||
results.sort((a, b) => (b._score || 0) - (a._score || 0))
|
|
||||||
return results.slice(0, limit)
|
|
||||||
},
|
|
||||||
searchResults: async (_parent, args, context, _resolveInfo) => {
|
searchResults: async (_parent, args, context, _resolveInfo) => {
|
||||||
const { query, limit } = args
|
const { query, limit } = args
|
||||||
const userId = context.user?.id || null
|
const userId = context.user?.id || null
|
||||||
|
|||||||
@ -245,6 +245,8 @@ describe('UpdateUser', () => {
|
|||||||
locationName: 'Hamburg, New Jersey, United States',
|
locationName: 'Hamburg, New Jersey, United States',
|
||||||
location: expect.objectContaining({
|
location: expect.objectContaining({
|
||||||
name: 'Hamburg',
|
name: 'Hamburg',
|
||||||
|
nameDE: 'Hamburg',
|
||||||
|
nameEN: 'Hamburg',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -354,11 +356,13 @@ describe('Delete a User as admin', () => {
|
|||||||
{
|
{
|
||||||
id: 'p139',
|
id: 'p139',
|
||||||
content: 'Post by user u343',
|
content: 'Post by user u343',
|
||||||
|
contentExcerpt: 'Post by user u343',
|
||||||
deleted: false,
|
deleted: false,
|
||||||
comments: [
|
comments: [
|
||||||
{
|
{
|
||||||
id: 'c156',
|
id: 'c156',
|
||||||
content: "A comment by someone else on user u343's post",
|
content: "A comment by someone else on user u343's post",
|
||||||
|
contentExcerpt: "A comment by someone else on user u343's post",
|
||||||
deleted: false,
|
deleted: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -368,6 +372,7 @@ describe('Delete a User as admin', () => {
|
|||||||
{
|
{
|
||||||
id: 'c155',
|
id: 'c155',
|
||||||
content: 'Comment by user u343',
|
content: 'Comment by user u343',
|
||||||
|
contentExcerpt: 'Comment by user u343',
|
||||||
deleted: false,
|
deleted: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -397,11 +402,13 @@ describe('Delete a User as admin', () => {
|
|||||||
{
|
{
|
||||||
id: 'p139',
|
id: 'p139',
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
comments: [
|
comments: [
|
||||||
{
|
{
|
||||||
id: 'c156',
|
id: 'c156',
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -411,6 +418,7 @@ describe('Delete a User as admin', () => {
|
|||||||
{
|
{
|
||||||
id: 'c155',
|
id: 'c155',
|
||||||
content: 'UNAVAILABLE',
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
deleted: true,
|
deleted: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { getNeode } from '@db/neo4j'
|
|||||||
import { UserInputError, ForbiddenError } from '@graphql/errors'
|
import { UserInputError, ForbiddenError } from '@graphql/errors'
|
||||||
|
|
||||||
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
|
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
|
||||||
import { filterUsersHasLocation } from './helpers/filterHasLocation'
|
|
||||||
import normalizeEmail from './helpers/normalizeEmail'
|
import normalizeEmail from './helpers/normalizeEmail'
|
||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
import { images } from './images/images'
|
import { images } from './images/images'
|
||||||
@ -67,7 +66,6 @@ export default {
|
|||||||
await session.close()
|
await session.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args = await filterUsersHasLocation(args, context)
|
|
||||||
return neo4jgraphql(object, args, context, resolveInfo)
|
return neo4jgraphql(object, args, context, resolveInfo)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -225,6 +223,7 @@ export default {
|
|||||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||||
SET resource.deleted = true
|
SET resource.deleted = true
|
||||||
SET resource.content = 'UNAVAILABLE'
|
SET resource.content = 'UNAVAILABLE'
|
||||||
|
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||||
SET resource.language = 'UNAVAILABLE'
|
SET resource.language = 'UNAVAILABLE'
|
||||||
SET resource.createdAt = 'UNAVAILABLE'
|
SET resource.createdAt = 'UNAVAILABLE'
|
||||||
SET resource.updatedAt = 'UNAVAILABLE'
|
SET resource.updatedAt = 'UNAVAILABLE'
|
||||||
|
|||||||
@ -34,7 +34,6 @@ const newlyCreatedNodesWithLocales = [
|
|||||||
nameRU: 'Вельцхайм',
|
nameRU: 'Вельцхайм',
|
||||||
nameNL: 'Welzheim',
|
nameNL: 'Welzheim',
|
||||||
namePL: 'Welzheim',
|
namePL: 'Welzheim',
|
||||||
nameSQ: 'Welzheim',
|
|
||||||
lng: 9.634301,
|
lng: 9.634301,
|
||||||
lat: 48.874393,
|
lat: 48.874393,
|
||||||
},
|
},
|
||||||
@ -51,7 +50,6 @@ const newlyCreatedNodesWithLocales = [
|
|||||||
namePL: 'Badenia-Wirtembergia',
|
namePL: 'Badenia-Wirtembergia',
|
||||||
namePT: 'Baden-Württemberg',
|
namePT: 'Baden-Württemberg',
|
||||||
nameRU: 'Баден-Вюртемберг',
|
nameRU: 'Баден-Вюртемберг',
|
||||||
nameSQ: 'Baden-Vyrtemberg',
|
|
||||||
},
|
},
|
||||||
country: {
|
country: {
|
||||||
id: expect.stringContaining('country'),
|
id: expect.stringContaining('country'),
|
||||||
@ -66,7 +64,6 @@ const newlyCreatedNodesWithLocales = [
|
|||||||
namePL: 'Niemcy',
|
namePL: 'Niemcy',
|
||||||
namePT: 'Alemanha',
|
namePT: 'Alemanha',
|
||||||
nameRU: 'Германия',
|
nameRU: 'Германия',
|
||||||
nameSQ: 'Gjermania',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { UserInputError } from '@graphql/errors'
|
|||||||
|
|
||||||
import type { Context } from '@src/context'
|
import type { Context } from '@src/context'
|
||||||
|
|
||||||
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru', 'sq']
|
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru']
|
||||||
|
|
||||||
const REQUEST_TIMEOUT = 3000
|
const REQUEST_TIMEOUT = 3000
|
||||||
|
|
||||||
@ -29,7 +29,6 @@ const createLocation = async (session, mapboxData) => {
|
|||||||
namePT: mapboxData.text_pt,
|
namePT: mapboxData.text_pt,
|
||||||
namePL: mapboxData.text_pl,
|
namePL: mapboxData.text_pl,
|
||||||
nameRU: mapboxData.text_ru,
|
nameRU: mapboxData.text_ru,
|
||||||
nameSQ: mapboxData.text_sq,
|
|
||||||
type: mapboxData.id.split('.')[0].toLowerCase(),
|
type: mapboxData.id.split('.')[0].toLowerCase(),
|
||||||
address: mapboxData.address,
|
address: mapboxData.address,
|
||||||
lng: mapboxData.center?.length ? mapboxData.center[0] : null,
|
lng: mapboxData.center?.length ? mapboxData.center[0] : null,
|
||||||
@ -48,7 +47,6 @@ const createLocation = async (session, mapboxData) => {
|
|||||||
'l.namePT = $namePT, ' +
|
'l.namePT = $namePT, ' +
|
||||||
'l.namePL = $namePL, ' +
|
'l.namePL = $namePL, ' +
|
||||||
'l.nameRU = $nameRU, ' +
|
'l.nameRU = $nameRU, ' +
|
||||||
'l.nameSQ = $nameSQ, ' +
|
|
||||||
'l.type = $type'
|
'l.type = $type'
|
||||||
|
|
||||||
if (data.lat && data.lng) {
|
if (data.lat && data.lng) {
|
||||||
|
|||||||
@ -41,6 +41,7 @@ type Comment {
|
|||||||
activityId: String
|
activityId: String
|
||||||
author: User @relation(name: "WROTE", direction: "IN")
|
author: User @relation(name: "WROTE", direction: "IN")
|
||||||
content: String!
|
content: String!
|
||||||
|
contentExcerpt: String
|
||||||
post: Post @relation(name: "COMMENTS", direction: "OUT")
|
post: Post @relation(name: "COMMENTS", direction: "OUT")
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
@ -80,7 +81,7 @@ type Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateComment(id: ID, postId: ID!, content: String!): Comment
|
CreateComment(id: ID, postId: ID!, content: String!, contentExcerpt: String): Comment
|
||||||
UpdateComment(id: ID!, content: String!): Comment
|
UpdateComment(id: ID!, content: String!, contentExcerpt: String): Comment
|
||||||
DeleteComment(id: ID!): Comment
|
DeleteComment(id: ID!): Comment
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,6 @@ input _GroupFilter {
|
|||||||
type Query {
|
type Query {
|
||||||
Group(
|
Group(
|
||||||
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
||||||
hasLocation: Boolean # if 'true' then only groups with a location
|
|
||||||
id: ID
|
id: ID
|
||||||
slug: String
|
slug: String
|
||||||
first: Int
|
first: Int
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
type Location {
|
type Location {
|
||||||
id: ID!
|
id: ID!
|
||||||
name(lang: String = ""): String!
|
name: String!
|
||||||
@cypher(
|
nameEN: String
|
||||||
statement: """
|
nameDE: String
|
||||||
RETURN COALESCE(
|
nameFR: String
|
||||||
CASE WHEN $lang <> '' THEN this['name' + toUpper($lang)] END,
|
nameNL: String
|
||||||
this['name' + $cypherParams.languageDefault],
|
nameIT: String
|
||||||
this.name,
|
nameES: String
|
||||||
this.nameEN,
|
namePT: String
|
||||||
this.id
|
namePL: String
|
||||||
)
|
nameRU: String
|
||||||
"""
|
|
||||||
)
|
|
||||||
type: String!
|
type: String!
|
||||||
lat: Float
|
lat: Float
|
||||||
lng: Float
|
lng: Float
|
||||||
|
|||||||
@ -28,37 +28,19 @@ type Message {
|
|||||||
saved: Boolean
|
saved: Boolean
|
||||||
distributed: Boolean
|
distributed: Boolean
|
||||||
seen: Boolean
|
seen: Boolean
|
||||||
@cypher(
|
|
||||||
statement: """
|
|
||||||
MATCH (this)<-[:CREATED]-(author:User)
|
|
||||||
OPTIONAL MATCH (unseer:User)-[:HAS_NOT_SEEN]->(this)
|
|
||||||
WHERE CASE
|
|
||||||
WHEN author.id = $cypherParams.currentUserId THEN true
|
|
||||||
ELSE unseer.id = $cypherParams.currentUserId
|
|
||||||
END
|
|
||||||
RETURN count(unseer) = 0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
files: [File]! @relation(name: "ATTACHMENT", direction: "OUT")
|
files: [File]! @relation(name: "ATTACHMENT", direction: "OUT")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateMessage(roomId: ID, userId: ID, content: String, files: [FileInput]): Message
|
CreateMessage(roomId: ID!, content: String, files: [FileInput]): Message
|
||||||
|
|
||||||
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Message(roomId: ID!, first: Int, offset: Int, beforeIndex: Int, orderBy: [_MessageOrdering]): [Message]
|
Message(roomId: ID!, first: Int, offset: Int, orderBy: [_MessageOrdering]): [Message]
|
||||||
}
|
|
||||||
|
|
||||||
type ChatMessageStatusPayload {
|
|
||||||
roomId: ID!
|
|
||||||
messageIds: [String!]!
|
|
||||||
status: String!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
chatMessageAdded: Message
|
chatMessageAdded: Message
|
||||||
chatMessageStatusUpdated: ChatMessageStatusPayload
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,6 @@ input _PostFilter {
|
|||||||
postType_in: [PostType]
|
postType_in: [PostType]
|
||||||
eventStart_gte: String
|
eventStart_gte: String
|
||||||
eventEnd_gte: String
|
eventEnd_gte: String
|
||||||
hasLocation: Boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _PostOrdering {
|
enum _PostOrdering {
|
||||||
@ -130,6 +129,7 @@ type Post {
|
|||||||
title: String!
|
title: String!
|
||||||
slug: String!
|
slug: String!
|
||||||
content: String!
|
content: String!
|
||||||
|
contentExcerpt: String
|
||||||
image: Image @relation(name: "HERO_IMAGE", direction: "OUT")
|
image: Image @relation(name: "HERO_IMAGE", direction: "OUT")
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
@ -230,6 +230,7 @@ type Mutation {
|
|||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
|
contentExcerpt: String
|
||||||
groupId: ID
|
groupId: ID
|
||||||
postType: PostType = Article
|
postType: PostType = Article
|
||||||
eventInput: _EventInput
|
eventInput: _EventInput
|
||||||
@ -239,6 +240,7 @@ type Mutation {
|
|||||||
title: String!
|
title: String!
|
||||||
slug: String
|
slug: String
|
||||||
content: String!
|
content: String!
|
||||||
|
contentExcerpt: String
|
||||||
image: ImageInput
|
image: ImageInput
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
language: String
|
language: String
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
|
# input _RoomFilter {
|
||||||
|
# AND: [_RoomFilter!]
|
||||||
|
# OR: [_RoomFilter!]
|
||||||
|
# id: ID
|
||||||
|
# users_some: _UserFilter
|
||||||
|
# }
|
||||||
|
|
||||||
|
# TODO change this to last message date
|
||||||
|
|
||||||
enum _RoomOrdering {
|
enum _RoomOrdering {
|
||||||
lastMessageAt_desc
|
lastMessageAt_desc
|
||||||
createdAt_desc
|
createdAt_desc
|
||||||
@ -9,36 +18,19 @@ type Room {
|
|||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
|
||||||
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
||||||
group: Group @relation(name: "ROOM_FOR", direction: "OUT")
|
|
||||||
|
|
||||||
roomId: String! @cypher(statement: "RETURN this.id")
|
roomId: String! @cypher(statement: "RETURN this.id")
|
||||||
isGroupRoom: Boolean!
|
|
||||||
@cypher(
|
|
||||||
statement: """
|
|
||||||
OPTIONAL MATCH (this)-[:ROOM_FOR]->(g:Group)
|
|
||||||
RETURN g IS NOT NULL
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
roomName: String!
|
roomName: String!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name"
|
||||||
OPTIONAL MATCH (this)-[:ROOM_FOR]->(g:Group)
|
|
||||||
WITH this, g
|
|
||||||
OPTIONAL MATCH (this)<-[:CHATS_IN]-(user:User)
|
|
||||||
WHERE g IS NULL AND NOT user.id = $cypherParams.currentUserId
|
|
||||||
RETURN COALESCE(g.name, user.name)
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
avatar: String
|
avatar: String
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
OPTIONAL MATCH (this)-[:ROOM_FOR]->(g:Group)
|
MATCH (this)<-[:CHATS_IN]-(user:User)
|
||||||
OPTIONAL MATCH (g)-[:AVATAR_IMAGE]->(groupImg:Image)
|
WHERE NOT user.id = $cypherParams.currentUserId
|
||||||
WITH this, g, groupImg
|
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image)
|
||||||
OPTIONAL MATCH (this)<-[:CHATS_IN]-(user:User)
|
RETURN image.url
|
||||||
WHERE g IS NULL AND NOT user.id = $cypherParams.currentUserId
|
|
||||||
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(userImg:Image)
|
|
||||||
RETURN COALESCE(groupImg.url, userImg.url)
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,24 +45,23 @@ type Room {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
"Count unread messages, excluding those from blocked/muted senders"
|
|
||||||
unreadCount: Int
|
unreadCount: Int
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (u:User { id: $cypherParams.currentUserId })-[:HAS_NOT_SEEN]->(message:Message)-[:INSIDE]->(this)
|
MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User)
|
||||||
MATCH (message)<-[:CREATED]-(sender:User)
|
WHERE NOT user.id = $cypherParams.currentUserId
|
||||||
WHERE NOT (u)-[:BLOCKED]->(sender) AND NOT (u)-[:MUTED]->(sender)
|
AND NOT message.seen
|
||||||
RETURN count(message)
|
RETURN count(message)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
CreateGroupRoom(groupId: ID!): Room
|
CreateRoom(userId: ID!): Room
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
Room(id: ID, userId: ID, groupId: ID, first: Int, before: String, orderBy: [_RoomOrdering]): [Room]
|
Room(id: ID, orderBy: [_RoomOrdering]): [Room]
|
||||||
UnreadRooms: Int
|
UnreadRooms: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
union SearchResult = Post | User | Tag | Group
|
union SearchResult = Post | User | Tag | Group
|
||||||
union ChatTarget = User | Group
|
|
||||||
|
|
||||||
type postSearchResults {
|
type postSearchResults {
|
||||||
postCount: Int
|
postCount: Int
|
||||||
@ -27,5 +26,4 @@ type Query {
|
|||||||
searchGroups(query: String!, firstGroups: Int, groupsOffset: Int): groupSearchResults!
|
searchGroups(query: String!, firstGroups: Int, groupsOffset: Int): groupSearchResults!
|
||||||
searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
|
searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
|
||||||
searchResults(query: String!, limit: Int = 5): [SearchResult]!
|
searchResults(query: String!, limit: Int = 5): [SearchResult]!
|
||||||
searchChatTargets(query: String!, limit: Int = 10): [ChatTarget]!
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,7 +158,6 @@ type User {
|
|||||||
input _UserFilter {
|
input _UserFilter {
|
||||||
AND: [_UserFilter!]
|
AND: [_UserFilter!]
|
||||||
OR: [_UserFilter!]
|
OR: [_UserFilter!]
|
||||||
hasLocation: Boolean
|
|
||||||
name_contains: String
|
name_contains: String
|
||||||
about_contains: String
|
about_contains: String
|
||||||
slug_contains: String
|
slug_contains: String
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user