From 6b14995ef59cb9d1e8bab59d8a6b870e82ccf5be Mon Sep 17 00:00:00 2001 From: shadrach Date: Thu, 9 Jan 2025 11:48:24 +0100 Subject: [PATCH] update radar and feed apis with new impl --- .../src/controllers/communities/feed.ts | 36 ++++++++++- .../src/controllers/communities/radar.ts | 25 ++++---- .../src/controllers/communities/types.ts | 4 +- .../src/controllers/communities/util.ts | 60 +++++++++++++++++++ .../src/routes/v1/communities/index.ts | 16 +++-- .../src/routes/v1/communities/schema.ts | 3 + desci-server/src/services/Communities.ts | 2 +- 7 files changed, 129 insertions(+), 17 deletions(-) diff --git a/desci-server/src/controllers/communities/feed.ts b/desci-server/src/controllers/communities/feed.ts index eb73327e2..5743b6032 100644 --- a/desci-server/src/controllers/communities/feed.ts +++ b/desci-server/src/controllers/communities/feed.ts @@ -1,14 +1,16 @@ import { NextFunction, Request, Response } from 'express'; import _ from 'lodash'; +import z from 'zod'; import { NotFoundError } from '../../core/ApiError.js'; import { SuccessResponse } from '../../core/ApiResponse.js'; import { logger as parentLogger } from '../../logger.js'; +import { getCommunityFeedSchema } from '../../routes/v1/communities/schema.js'; import { attestationService } from '../../services/Attestation.js'; import { communityService } from '../../services/Communities.js'; import { asyncMap } from '../../utils.js'; -import { resolveLatestNode } from './util.js'; +import { getCommunityNodeDetails, resolveLatestNode } from './util.js'; const logger = parentLogger.child({ module: 'communities/feed.ts' }); @@ -46,6 +48,38 @@ export const getCommunityFeed = async (req: Request, res: Response, next: NextFu return new SuccessResponse(data).send(res); }; +export const listCommunityFeed = async (req: Request, res: Response, next: NextFunction) => { + const { query, params } = await getCommunityFeedSchema.parseAsync(req); + const limit = 20; + const page = Math.max(Math.max((query.cursor ?? 0) - 1, 0), 0); + const offset = limit * page; + + const curatedNodes = await communityService.listCommunityCuratedFeed({ + communityId: parseInt(params.communityId.toString()), + offset, + limit, + }); + logger.trace({ offset, page, cursor: query.cursor }, 'Feed'); + // THIS is necessary because the engagement signal returned from getcuratedNodes + // accounts for only engagements on community selected attestations + const entries = await asyncMap(curatedNodes, async (entry) => { + const engagements = await attestationService.getNodeEngagementSignalsByUuid(entry.nodeUuid); + return { + ...entry, + engagements, + verifiedEngagements: { + reactions: entry.reactions, + annotations: entry.annotations, + verifications: entry.verifications, + }, + }; + }); + + const data = await Promise.all(entries.map(getCommunityNodeDetails)); + // logger.info({ count: data.length, page: offset }, 'listCommunityFeed'); + return new SuccessResponse({ count: data.length, cursor: page + 1, data }).send(res); +}; + export const getCommunityDetails = async (req: Request, res: Response, next: NextFunction) => { const community = await communityService.findCommunityByNameOrSlug(req.params.communityName as string); diff --git a/desci-server/src/controllers/communities/radar.ts b/desci-server/src/controllers/communities/radar.ts index 743bb1173..91b2f8bcd 100644 --- a/desci-server/src/controllers/communities/radar.ts +++ b/desci-server/src/controllers/communities/radar.ts @@ -4,11 +4,12 @@ import _ from 'lodash'; import { SuccessResponse } from '../../core/ApiResponse.js'; import { logger as parentLogger } from '../../logger.js'; +import { getCommunityFeedSchema } from '../../routes/v1/communities/schema.js'; import { attestationService } from '../../services/Attestation.js'; import { communityService } from '../../services/Communities.js'; import { asyncMap } from '../../utils.js'; -import { resolveLatestNode } from './util.js'; +import { getCommunityNodeDetails, resolveLatestNode } from './util.js'; const logger = parentLogger.child({ module: 'GET COMMUNITY RADAR' }); export const getCommunityRadar = async (req: Request, res: Response, next: NextFunction) => { @@ -62,15 +63,20 @@ export const getCommunityRadar = async (req: Request, res: Response, next: NextF }; export const listCommunityRadar = async (req: Request, res: Response, next: NextFunction) => { + const { query, params } = await getCommunityFeedSchema.parseAsync(req); + const limit = 20; + const page = Math.max(Math.max((query.cursor ?? 0) - 1, 0), 0); + const offset = limit * page; + const communityRadar = await communityService.listCommunityRadar({ - communityId: parseInt(req.params.communityId as string), - offset: 0, - limit: 20, + communityId: parseInt(params.communityId.toString()), + offset, + limit, }); - logger.info({ communityRadar }, 'Radar'); + logger.trace({ offset, page, cursor: query.cursor }, 'Radar'); // THIS is necessary because the engagement signal returned from getCommunityRadar // accounts for only engagements on community selected attestations - const nodes = await asyncMap(communityRadar, async (entry) => { + const entries = await asyncMap(communityRadar, async (entry) => { const engagements = await attestationService.getNodeEngagementSignalsByUuid(entry.nodeUuid); return { ...entry, @@ -84,8 +90,7 @@ export const listCommunityRadar = async (req: Request, res: Response, next: Next }); // rank nodes by sum of sum of verified and non verified signals - - logger.info({ nodes }, 'CHECK Verification SignalS'); - - return new SuccessResponse(nodes).send(res); + const data = await Promise.all(entries.map(getCommunityNodeDetails)); + // logger.trace({ count: data.length }, 'listCommunityRadar'); + return new SuccessResponse({ count: data.length, cursor: page + 1, data }).send(res); }; diff --git a/desci-server/src/controllers/communities/types.ts b/desci-server/src/controllers/communities/types.ts index b66657b9b..b635f058b 100644 --- a/desci-server/src/controllers/communities/types.ts +++ b/desci-server/src/controllers/communities/types.ts @@ -1,7 +1,7 @@ import { ResearchObjectV1 } from '@desci-labs/desci-models'; import { Node } from '@prisma/client'; -import { CommunityRadarNode } from '../../services/Communities.js'; +import { CommunityRadarNode, RadarEntry } from '../../services/Communities.js'; export type NodeRadarItem = { NodeAttestation: CommunityRadarNode[]; @@ -46,3 +46,5 @@ export type NodeRadar = NodeRadarItem & { verifications: number; }; }; + +export type NodeRadarEntry = RadarEntry & { node?: Partial; manifest?: ResearchObjectV1 }; diff --git a/desci-server/src/controllers/communities/util.ts b/desci-server/src/controllers/communities/util.ts index c5269399f..958c795d6 100644 --- a/desci-server/src/controllers/communities/util.ts +++ b/desci-server/src/controllers/communities/util.ts @@ -1,9 +1,11 @@ +import { ResearchObjectV1 } from '@desci-labs/desci-models'; import { Node, NodeVersion } from '@prisma/client'; import axios from 'axios'; import _ from 'lodash'; import { prisma } from '../../client.js'; import { logger } from '../../logger.js'; +import { RadarEntry } from '../../services/Communities.js'; import { NodeUuid } from '../../services/manifestRepo.js'; import repoService from '../../services/repoService.js'; import { IndexedResearchObject, getIndexedResearchObjects } from '../../theGraph.js'; @@ -68,6 +70,64 @@ export const resolveLatestNode = async (radar: Partial) => { return radar; }; +export const getCommunityNodeDetails = async ( + radar: RadarEntry & { node?: Partial; manifest?: ResearchObjectV1 }, +) => { + const uuid = ensureUuidEndsWithDot(radar.nodeUuid); + + const discovery = await prisma.node.findFirst({ + where: { + uuid, + isDeleted: false, + }, + select: { + id: true, + manifestUrl: true, + ownerId: true, + uuid: true, + title: true, + NodeCover: true, + }, + }); + + if (!discovery) { + logger.warn({ uuid }, 'uuid not found'); + } + + const selectAttributes: (keyof typeof discovery)[] = ['ownerId', 'NodeCover']; + const node: Partial = _.pick(discovery, selectAttributes); + const publishedVersions = + (await prisma.$queryRaw`SELECT * from "NodeVersion" where "nodeId" = ${discovery.id} AND ("transactionId" IS NOT NULL or "commitId" IS NOT NULL) ORDER BY "createdAt" DESC`) as NodeVersion[]; + + // const nodeVersions = (await getNodeVersion + logger.info({ uuid: discovery.uuid, publishedVersions }, 'Resolve node'); + node['versions'] = publishedVersions.length; + node['publishedDate'] = publishedVersions[0].createdAt; + node.manifestUrl = publishedVersions[0].manifestUrl; + radar.node = node; + + let gatewayUrl = publishedVersions[0].manifestUrl; + + try { + gatewayUrl = cleanupManifestUrl(gatewayUrl); + // logger.trace({ gatewayUrl, uuid }, 'transforming manifest'); + const manifest = (await axios.get(gatewayUrl)).data; + radar.manifest = manifest; + + // logger.info({ manifest }, '[SHOW API GET LAST PUBLISHED MANIFEST]'); + } catch (err) { + const manifest = await repoService.getDraftManifest({ + uuid: node.uuid as NodeUuid, + documentId: node.manifestDocumentId, + }); + radar.manifest = manifest; + logger.error({ err, manifestUrl: discovery.manifestUrl, gatewayUrl }, 'nodes/show.ts: failed to preload manifest'); + } + + radar.node = { ...radar.node, ...node }; + return radar; +}; + export const getNodeVersion = async (uuid: string) => { let indexingResults: { researchObjects: IndexedResearchObject[] }; try { diff --git a/desci-server/src/routes/v1/communities/index.ts b/desci-server/src/routes/v1/communities/index.ts index ba157424c..0e6f1b3a2 100644 --- a/desci-server/src/routes/v1/communities/index.ts +++ b/desci-server/src/routes/v1/communities/index.ts @@ -4,10 +4,15 @@ import { getCommunityRecommendations, getValidatedAttestations, } from '../../../controllers/attestations/recommendations.js'; -import { getAllFeeds, getCommunityDetails, getCommunityFeed } from '../../../controllers/communities/feed.js'; +import { + getAllFeeds, + getCommunityDetails, + getCommunityFeed, + listCommunityFeed, +} from '../../../controllers/communities/feed.js'; import { checkMemberGuard } from '../../../controllers/communities/guard.js'; import { listCommunities } from '../../../controllers/communities/list.js'; -import { getCommunityRadar } from '../../../controllers/communities/radar.js'; +import { getCommunityRadar, listCommunityRadar } from '../../../controllers/communities/radar.js'; import { ensureUser } from '../../../middleware/permissions.js'; import { validate } from '../../../middleware/validator.js'; import { asyncHandler } from '../../../utils/asyncHandler.js'; @@ -32,8 +37,11 @@ router.get( asyncHandler(getValidatedAttestations), ); -router.get('/:communityId/feed', [validate(getCommunityFeedSchema)], asyncHandler(getCommunityFeed)); -router.get('/:communityId/radar', [validate(getCommunityFeedSchema)], asyncHandler(getCommunityRadar)); +// router.get('/:communityId/feed', [validate(getCommunityFeedSchema)], asyncHandler(getCommunityFeed)); +// router.get('/:communityId/radar', [validate(getCommunityFeedSchema)], asyncHandler(getCommunityRadar)); + +router.get('/:communityId/feed', [validate(getCommunityFeedSchema)], asyncHandler(listCommunityFeed)); +router.get('/:communityId/radar', [validate(getCommunityFeedSchema)], asyncHandler(listCommunityRadar)); router.post('/:communityId/memberGuard', [ensureUser, validate(memberGuardSchema)], asyncHandler(checkMemberGuard)); diff --git a/desci-server/src/routes/v1/communities/schema.ts b/desci-server/src/routes/v1/communities/schema.ts index 39ec76e7a..1c2ece543 100644 --- a/desci-server/src/routes/v1/communities/schema.ts +++ b/desci-server/src/routes/v1/communities/schema.ts @@ -10,6 +10,9 @@ export const getCommunityFeedSchema = z.object({ params: z.object({ communityId: z.coerce.number(), }), + query: z.object({ + cursor: z.coerce.number().optional().default(0), + }), }); export const memberGuardSchema = z.object({ diff --git a/desci-server/src/services/Communities.ts b/desci-server/src/services/Communities.ts index f95d15991..3aab0601f 100644 --- a/desci-server/src/services/Communities.ts +++ b/desci-server/src/services/Communities.ts @@ -233,7 +233,7 @@ export class CommunityService { verifications ASC, cre."createdAt" DESC LIMIT - ${limit}; + ${limit} OFFSET ${offset}; `;