mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
## 🍰 Pullrequest This will migrate our assets to an objectstorage via S3. Before this PR is rolled out, the S3 credentials need to be configured in the respective infrastructure repository. The migration is implemented in a backend migration, i.e. I expect the `initContainer` to take a little longer but I hope then it's going to be fine. If any errors occcur, the migration should be repeatable, since the disk volume is still there. ### Issues The backend having direct access on disk. ### Todo - [ ] Configure backend environment variables in every infrastructure repo - [ ] Remove kubernetes uploads volume in a future PR Commits: * refactor: follow @ulfgebhardt Here: https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8545#pullrequestreview-2846163417 I don't know why the PR didn't include these changes already, I believe I made a mistake during rebase and lost the relevant commits. * refactor: use typescript assertions I found it a better way to react to this comment: https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8545/files#r2092766596 * add S3 credentials * refactor: easier to remember credentials It's for local development only * give init container necessary file access * fix: wrong upload location on production * refactor: follow @ulfgebhardt's review See: https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8545#pullrequestreview-2881626504
572 lines
21 KiB
TypeScript
572 lines
21 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
import { UserInputError } from 'apollo-server'
|
|
import { isEmpty } from 'lodash'
|
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
|
import { v4 as uuid } from 'uuid'
|
|
|
|
import CONFIG from '@config/index'
|
|
import { Context } from '@src/server'
|
|
|
|
import { validateEventParams } from './helpers/events'
|
|
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
|
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
|
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
|
|
import Resolver from './helpers/Resolver'
|
|
import { images } from './images/images'
|
|
import { createOrUpdateLocations } from './users/location'
|
|
|
|
const maintainPinnedPosts = (params) => {
|
|
const pinnedPostFilter = { pinned: true }
|
|
if (isEmpty(params.filter)) {
|
|
params.filter = { OR: [pinnedPostFilter, {}] }
|
|
} else {
|
|
params.filter = { OR: [pinnedPostFilter, { ...params.filter }] }
|
|
}
|
|
return params
|
|
}
|
|
|
|
const filterEventDates = (params) => {
|
|
if (params.filter?.eventStart_gte) {
|
|
const date = params.filter.eventStart_gte
|
|
delete params.filter.eventStart_gte
|
|
params.filter = { ...params.filter, OR: [{ eventStart_gte: date }, { eventEnd_gte: date }] }
|
|
}
|
|
return params
|
|
}
|
|
|
|
export default {
|
|
Query: {
|
|
Post: async (object, params, context, resolveInfo) => {
|
|
params = await filterPostsOfMyGroups(params, context)
|
|
params = await filterInvisiblePosts(params, context)
|
|
params = await filterForMutedUsers(params, context)
|
|
params = filterEventDates(params)
|
|
params = await maintainPinnedPosts(params)
|
|
return neo4jgraphql(object, params, context, resolveInfo)
|
|
},
|
|
profilePagePosts: async (object, params, context, resolveInfo) => {
|
|
params = await filterPostsOfMyGroups(params, context)
|
|
params = await filterInvisiblePosts(params, context)
|
|
params = await filterForMutedUsers(params, context)
|
|
return neo4jgraphql(object, params, context, resolveInfo)
|
|
},
|
|
PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => {
|
|
const { postId, data } = params
|
|
const session = context.driver.session()
|
|
const readTxResultPromise = session.readTransaction(async (transaction) => {
|
|
const emotionsCountTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
|
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
|
`,
|
|
{ postId, data },
|
|
)
|
|
return emotionsCountTransactionResponse.records.map(
|
|
(record) => record.get('emotionsCount').low,
|
|
)
|
|
})
|
|
try {
|
|
const [emotionsCount] = await readTxResultPromise
|
|
return emotionsCount
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
PostsEmotionsByCurrentUser: async (_object, params, context, _resolveInfo) => {
|
|
const { postId } = params
|
|
const session = context.driver.session()
|
|
const readTxResultPromise = session.readTransaction(async (transaction) => {
|
|
const emotionsTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
|
RETURN collect(emoted.emotion) as emotion
|
|
`,
|
|
{ userId: context.user.id, postId },
|
|
)
|
|
return emotionsTransactionResponse.records.map((record) => record.get('emotion'))
|
|
})
|
|
try {
|
|
const [emotions] = await readTxResultPromise
|
|
return emotions
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
PostsPinnedCounts: async (_object, params, context: Context, _resolveInfo) => {
|
|
const [postsPinnedCount] = (
|
|
await context.database.query({
|
|
query: 'MATCH (p:Post { pinned: true }) RETURN COUNT (p) AS count',
|
|
})
|
|
).records.map((r) => Number(r.get('count').toString()))
|
|
return {
|
|
maxPinnedPosts: CONFIG.MAX_PINNED_POSTS,
|
|
currentlyPinnedPosts: postsPinnedCount,
|
|
}
|
|
},
|
|
},
|
|
Mutation: {
|
|
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
|
const { categoryIds, groupId } = params
|
|
const { image: imageInput } = params
|
|
|
|
const locationName = validateEventParams(params)
|
|
|
|
delete params.categoryIds
|
|
delete params.image
|
|
delete params.groupId
|
|
params.id = params.id || uuid()
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
let groupCypher = ''
|
|
if (groupId) {
|
|
groupCypher = `
|
|
WITH post MATCH (group:Group { id: $groupId })
|
|
MERGE (post)-[:IN]->(group)`
|
|
const groupTypeResponse = await transaction.run(
|
|
`
|
|
MATCH (group:Group { id: $groupId }) RETURN group.groupType AS groupType`,
|
|
{ groupId },
|
|
)
|
|
const [groupType] = groupTypeResponse.records.map((record) => record.get('groupType'))
|
|
if (groupType !== 'public')
|
|
groupCypher += `
|
|
WITH post, group
|
|
MATCH (user:User)-[membership:MEMBER_OF]->(group)
|
|
WHERE group.groupType IN ['closed', 'hidden']
|
|
AND membership.role IN ['usual', 'admin', 'owner']
|
|
WITH post, collect(user.id) AS userIds
|
|
OPTIONAL MATCH path =(restricted:User) WHERE NOT restricted.id IN userIds
|
|
FOREACH (user IN nodes(path) |
|
|
MERGE (user)-[:CANNOT_SEE]->(post)
|
|
)`
|
|
}
|
|
const categoriesCypher =
|
|
CONFIG.CATEGORIES_ACTIVE && categoryIds
|
|
? `WITH post
|
|
UNWIND $categoryIds AS categoryId
|
|
MATCH (category:Category {id: categoryId})
|
|
MERGE (post)-[:CATEGORIZED]->(category)`
|
|
: ''
|
|
const createPostTransactionResponse = await transaction.run(
|
|
`
|
|
CREATE (post:Post)
|
|
SET post += $params
|
|
SET post.createdAt = toString(datetime())
|
|
SET post.updatedAt = toString(datetime())
|
|
SET post.clickedCount = 0
|
|
SET post.viewedTeaserCount = 0
|
|
SET post:${params.postType}
|
|
WITH post
|
|
MATCH (author:User {id: $userId})
|
|
MERGE (post)<-[:WROTE]-(author)
|
|
MERGE (post)<-[obs:OBSERVES]-(author)
|
|
SET obs.active = true
|
|
SET obs.createdAt = toString(datetime())
|
|
SET obs.updatedAt = toString(datetime())
|
|
${categoriesCypher}
|
|
${groupCypher}
|
|
RETURN post {.*, postType: [l IN labels(post) WHERE NOT l = 'Post'] }
|
|
`,
|
|
{ userId: context.user.id, categoryIds, groupId, params },
|
|
)
|
|
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
|
|
if (imageInput) {
|
|
await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
|
|
}
|
|
return post
|
|
})
|
|
try {
|
|
const post = await writeTxResultPromise
|
|
if (locationName) {
|
|
await createOrUpdateLocations('Post', post.id, locationName, session)
|
|
}
|
|
return post
|
|
} catch (e) {
|
|
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
|
throw new UserInputError('Post with this slug already exists!')
|
|
throw new Error(e)
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
|
const { categoryIds } = params
|
|
const { image: imageInput } = params
|
|
|
|
const locationName = validateEventParams(params)
|
|
|
|
delete params.categoryIds
|
|
delete params.image
|
|
const session = context.driver.session()
|
|
let updatePostCypher = `
|
|
MATCH (post:Post {id: $params.id})
|
|
SET post += $params
|
|
SET post.updatedAt = toString(datetime())
|
|
WITH post
|
|
`
|
|
|
|
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
|
const cypherDeletePreviousRelations = `
|
|
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
|
DELETE previousRelations
|
|
RETURN post, category
|
|
`
|
|
|
|
await session.writeTransaction((transaction) => {
|
|
return transaction.run(cypherDeletePreviousRelations, { params })
|
|
})
|
|
|
|
updatePostCypher += `
|
|
UNWIND $categoryIds AS categoryId
|
|
MATCH (category:Category {id: categoryId})
|
|
MERGE (post)-[:CATEGORIZED]->(category)
|
|
WITH post
|
|
`
|
|
}
|
|
|
|
if (params.postType) {
|
|
updatePostCypher += `
|
|
REMOVE post:Article
|
|
REMOVE post:Event
|
|
SET post:${params.postType}
|
|
WITH post
|
|
`
|
|
}
|
|
|
|
updatePostCypher += `RETURN post {.*, postType: [l IN labels(post) WHERE NOT l = 'Post']}`
|
|
const updatePostVariables = { categoryIds, params }
|
|
try {
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const updatePostTransactionResponse = await transaction.run(
|
|
updatePostCypher,
|
|
updatePostVariables,
|
|
)
|
|
const [post] = updatePostTransactionResponse.records.map((record) => record.get('post'))
|
|
await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
|
|
return post
|
|
})
|
|
const post = await writeTxResultPromise
|
|
if (locationName) {
|
|
await createOrUpdateLocations('Post', post.id, locationName, session)
|
|
}
|
|
return post
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
|
|
DeletePost: async (_object, args, context, _resolveInfo) => {
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const deletePostTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (post:Post {id: $postId})
|
|
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
|
SET post.deleted = TRUE
|
|
SET post.content = 'UNAVAILABLE'
|
|
SET post.contentExcerpt = 'UNAVAILABLE'
|
|
SET post.title = 'UNAVAILABLE'
|
|
SET comment.deleted = TRUE
|
|
RETURN post {.*}
|
|
`,
|
|
{ postId: args.id },
|
|
)
|
|
const [post] = deletePostTransactionResponse.records.map((record) => record.get('post'))
|
|
await images.deleteImage(post, 'HERO_IMAGE', { transaction })
|
|
return post
|
|
})
|
|
try {
|
|
const post = await writeTxResultPromise
|
|
return post
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
AddPostEmotions: async (_object, params, context, _resolveInfo) => {
|
|
const { to, data } = params
|
|
const { user } = context
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const addPostEmotionsTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
|
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
|
RETURN userFrom, postTo, emotedRelation`,
|
|
{ user, to, data },
|
|
)
|
|
return addPostEmotionsTransactionResponse.records.map((record) => {
|
|
return {
|
|
from: { ...record.get('userFrom').properties },
|
|
to: { ...record.get('postTo').properties },
|
|
...record.get('emotedRelation').properties,
|
|
}
|
|
})
|
|
})
|
|
try {
|
|
const [emoted] = await writeTxResultPromise
|
|
return emoted
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
RemovePostEmotions: async (_object, params, context, _resolveInfo) => {
|
|
const { to, data } = params
|
|
const { id: from } = context.user
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const removePostEmotionsTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
|
DELETE emotedRelation
|
|
RETURN userFrom, postTo
|
|
`,
|
|
{ from, to, data },
|
|
)
|
|
return removePostEmotionsTransactionResponse.records.map((record) => {
|
|
return {
|
|
from: { ...record.get('userFrom').properties },
|
|
to: { ...record.get('postTo').properties },
|
|
emotion: data.emotion,
|
|
}
|
|
})
|
|
})
|
|
try {
|
|
const [emoted] = await writeTxResultPromise
|
|
return emoted
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
pinPost: async (_parent, params, context: Context, _resolveInfo) => {
|
|
if (CONFIG.MAX_PINNED_POSTS === 0) throw new Error('Pinned posts are not allowed!')
|
|
let pinnedPostWithNestedAttributes
|
|
const { driver, user } = context
|
|
const session = driver.session()
|
|
const { id: userId } = user
|
|
const pinPostCypher = `
|
|
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
|
|
MATCH (post:Post {id: $params.id})
|
|
WHERE NOT EXISTS((post)-[:IN]->(:Group)) OR
|
|
(post)-[:IN]->(:Group { groupType: 'public'})
|
|
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
|
|
SET post.pinned = true
|
|
RETURN post, pinned.createdAt as pinnedAt`
|
|
|
|
if (CONFIG.MAX_PINNED_POSTS === 1) {
|
|
let writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const deletePreviousRelationsResponse = await transaction.run(
|
|
`
|
|
MATCH (:User)-[previousRelations:PINNED]->(post:Post)
|
|
REMOVE post.pinned
|
|
DELETE previousRelations
|
|
RETURN post
|
|
`,
|
|
)
|
|
return deletePreviousRelationsResponse.records.map(
|
|
(record) => record.get('post').properties,
|
|
)
|
|
})
|
|
try {
|
|
await writeTxResultPromise
|
|
|
|
writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const pinPostTransactionResponse = await transaction.run(pinPostCypher, {
|
|
userId,
|
|
params,
|
|
})
|
|
return pinPostTransactionResponse.records.map((record) => ({
|
|
pinnedPost: record.get('post').properties,
|
|
pinnedAt: record.get('pinnedAt'),
|
|
}))
|
|
})
|
|
const [transactionResult] = await writeTxResultPromise
|
|
if (transactionResult) {
|
|
const { pinnedPost, pinnedAt } = transactionResult
|
|
pinnedPostWithNestedAttributes = {
|
|
...pinnedPost,
|
|
pinnedAt,
|
|
}
|
|
}
|
|
} finally {
|
|
await session.close()
|
|
}
|
|
return pinnedPostWithNestedAttributes
|
|
} else {
|
|
const [currentPinnedPostCount] = (
|
|
await context.database.query({
|
|
query: `MATCH (:User)-[:PINNED]->(post:Post { pinned: true }) RETURN COUNT(post) AS count`,
|
|
})
|
|
).records.map((r) => Number(r.get('count').toString()))
|
|
if (currentPinnedPostCount >= CONFIG.MAX_PINNED_POSTS) {
|
|
throw new Error('Max number of pinned posts is reached!')
|
|
}
|
|
const [pinPostResult] = (
|
|
await context.database.write({
|
|
query: pinPostCypher,
|
|
variables: { userId, params },
|
|
})
|
|
).records.map((r) => ({
|
|
...r.get('post').properties,
|
|
pinnedAt: r.get('pinnedAt'),
|
|
}))
|
|
return pinPostResult
|
|
}
|
|
},
|
|
unpinPost: async (_parent, params, context, _resolveInfo) => {
|
|
let unpinnedPost
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const unpinPostTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (:User)-[previousRelations:PINNED]->(post:Post {id: $params.id})
|
|
REMOVE post.pinned
|
|
DELETE previousRelations
|
|
RETURN post
|
|
`,
|
|
{ params },
|
|
)
|
|
return unpinPostTransactionResponse.records.map((record) => record.get('post').properties)
|
|
})
|
|
try {
|
|
;[unpinnedPost] = await writeTxResultPromise
|
|
} finally {
|
|
session.close()
|
|
}
|
|
return unpinnedPost
|
|
},
|
|
markTeaserAsViewed: async (_parent, params, context, _resolveInfo) => {
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const transactionResponse = await transaction.run(
|
|
`
|
|
MATCH (post:Post { id: $params.id })
|
|
MATCH (user:User { id: $userId })
|
|
MERGE (user)-[relation:VIEWED_TEASER { }]->(post)
|
|
ON CREATE
|
|
SET relation.createdAt = toString(datetime()),
|
|
post.viewedTeaserCount = post.viewedTeaserCount + 1
|
|
RETURN post
|
|
`,
|
|
{ userId: context.user.id, params },
|
|
)
|
|
return transactionResponse.records.map((record) => record.get('post').properties)
|
|
})
|
|
try {
|
|
const [post] = await writeTxResultPromise
|
|
post.viewedTeaserCount = post.viewedTeaserCount.low
|
|
return post
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
toggleObservePost: async (_parent, params, context, _resolveInfo) => {
|
|
const session = context.driver.session()
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const transactionResponse = await transaction.run(
|
|
`
|
|
MATCH (post:Post { id: $params.id })
|
|
MATCH (user:User { id: $userId })
|
|
MERGE (user)-[obs:OBSERVES]->(post)
|
|
ON CREATE SET
|
|
obs.createdAt = toString(datetime()),
|
|
obs.updatedAt = toString(datetime()),
|
|
obs.active = $params.value
|
|
ON MATCH SET
|
|
obs.updatedAt = toString(datetime()),
|
|
obs.active = $params.value
|
|
RETURN post
|
|
`,
|
|
{ userId: context.user.id, params },
|
|
)
|
|
return transactionResponse.records.map((record) => record.get('post').properties)
|
|
})
|
|
try {
|
|
const [post] = await writeTxResultPromise
|
|
post.viewedTeaserCount = post.viewedTeaserCount.low
|
|
return post
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
},
|
|
Post: {
|
|
...Resolver('Post', {
|
|
undefinedToNull: [
|
|
'activityId',
|
|
'objectId',
|
|
'language',
|
|
'pinnedAt',
|
|
'pinned',
|
|
'eventVenue',
|
|
'eventLocation',
|
|
'eventLocationName',
|
|
'eventStart',
|
|
'eventEnd',
|
|
'eventIsOnline',
|
|
],
|
|
hasMany: {
|
|
tags: '-[:TAGGED]->(related:Tag)',
|
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
|
comments: '<-[:COMMENTS]-(related:Comment)',
|
|
shoutedBy: '<-[:SHOUTED]-(related:User)',
|
|
emotions: '<-[related:EMOTED]',
|
|
},
|
|
hasOne: {
|
|
author: '<-[:WROTE]-(related:User)',
|
|
pinnedBy: '<-[:PINNED]-(related:User)',
|
|
image: '-[:HERO_IMAGE]->(related:Image)',
|
|
group: '-[:IN]->(related:Group)',
|
|
eventLocation: '-[:IS_IN]->(related:Location)',
|
|
},
|
|
count: {
|
|
commentsCount:
|
|
'<-[:COMMENTS]-(related:Comment) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
|
shoutedCount:
|
|
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
|
emotionsCount: '<-[related:EMOTED]-(:User)',
|
|
observingUsersCount:
|
|
'<-[obs:OBSERVES]-(related:User) WHERE obs.active = true AND NOT related.deleted = true AND NOT related.disabled = true',
|
|
},
|
|
boolean: {
|
|
shoutedByCurrentUser:
|
|
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
|
viewedTeaserByCurrentUser:
|
|
'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
|
isObservedByMe:
|
|
'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1',
|
|
},
|
|
}),
|
|
relatedContributions: async (parent, _params, context, _resolveInfo) => {
|
|
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
|
const { id } = parent
|
|
const session = context.driver.session()
|
|
|
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
const relatedContributionsTransactionResponse = await transaction.run(
|
|
`
|
|
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
|
WHERE NOT post.deleted AND NOT post.disabled
|
|
RETURN DISTINCT post
|
|
LIMIT 10
|
|
`,
|
|
{ id },
|
|
)
|
|
return relatedContributionsTransactionResponse.records.map(
|
|
(record) => record.get('post').properties,
|
|
)
|
|
})
|
|
try {
|
|
const relatedContributions = await writeTxResultPromise
|
|
return relatedContributions
|
|
} finally {
|
|
session.close()
|
|
}
|
|
},
|
|
},
|
|
}
|