From 56eeab2ea85b2d885d9b19258be2509ac0f0a582 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 22 Nov 2024 12:14:01 +0000 Subject: [PATCH 1/9] draft wip raw query --- api/src/components/crosslink/service.ts | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/src/components/crosslink/service.ts b/api/src/components/crosslink/service.ts index cb9156bc0..6e93906bb 100644 --- a/api/src/components/crosslink/service.ts +++ b/api/src/components/crosslink/service.ts @@ -190,6 +190,34 @@ export const getVote = (crosslinkId: string, userId: string) => }); export const getPublicationCrosslinks = async (publicationId: string, options?: I.GetPublicationCrosslinksOptions) => { + /** + * Raw SQL query alternative: + +PREPARE getPublicationCrosslinks (text) AS + SELECT + c.id, + c."publicationToId" AS linkedPublicationId, + pv.title AS linkedPublicationTitle, + c."createdBy", + c."createdAt", + c.score + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationToId" = pv."publicationId" + WHERE c."publicationFromId" = $1 AND pv."isLatestLiveVersion" + UNION + SELECT + c.id, + c."publicationFromId" AS linkedPublicationId, + pv.title AS linkedPublicationTitle, + c."createdBy", + c."createdAt", + c.score + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationFromId" = pv."publicationId" + WHERE "publicationToId" = $1 AND pv."isLatestLiveVersion"; + +EXECUTE getPublicationCrosslinks('publicationId'); + */ const { order, search, limit, offset, userIdFilter } = options || {}; const publicationInclude = { select: { From c303788098c813a2375c796af0389bb5fa5e2ee3 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 6 Dec 2024 12:02:44 +0000 Subject: [PATCH 2/9] mostly working raw crosslinks query --- api/package-lock.json | 78 +++-- api/package.json | 4 +- api/prisma/schema.prisma | 2 +- api/src/components/crosslink/service.ts | 397 ++++++++++++------------ api/src/lib/interface.ts | 11 +- ui/src/lib/interfaces.ts | 1 - 6 files changed, 256 insertions(+), 237 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 416f6ae70..dfe4fffd9 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -14,7 +14,7 @@ "@middy/http-json-body-parser": "^4.7.0", "@opensearch-project/opensearch": "^2.5.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.11.0", + "@prisma/client": "^5.22.0", "@sparticuz/chromium": "^119.0.2", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -58,7 +58,7 @@ "jest": "^29.7.0", "lint-staged": "^13.3.0", "prettier": "^2.8.8", - "prisma": "^5.11.0", + "prisma": "^5.22.0", "puppeteer": "^22.12.0", "serverless": "^4.4.7", "serverless-offline": "^14.3.4", @@ -10097,10 +10097,11 @@ } }, "node_modules/@prisma/client": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz", - "integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { "node": ">=16.13" }, @@ -10114,48 +10115,53 @@ } }, "node_modules/@prisma/debug": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz", - "integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==", - "devOptional": true + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz", - "integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.11.0", - "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "@prisma/fetch-engine": "5.11.0", - "@prisma/get-platform": "5.11.0" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz", - "integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==", - "devOptional": true + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz", - "integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.11.0", - "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", - "@prisma/get-platform": "5.11.0" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz", - "integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.11.0" + "@prisma/debug": "5.22.0" } }, "node_modules/@puppeteer/browsers": { @@ -19678,19 +19684,23 @@ } }, "node_modules/prisma": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz", - "integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.11.0" + "@prisma/engines": "5.22.0" }, "bin": { "prisma": "build/index.js" }, "engines": { "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" } }, "node_modules/process-nextick-args": { diff --git a/api/package.json b/api/package.json index a287646ac..33f498a28 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,7 @@ "@middy/http-json-body-parser": "^4.7.0", "@opensearch-project/opensearch": "^2.5.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.11.0", + "@prisma/client": "^5.22.0", "@sparticuz/chromium": "^119.0.2", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -90,7 +90,7 @@ "jest": "^29.7.0", "lint-staged": "^13.3.0", "prettier": "^2.8.8", - "prisma": "^5.11.0", + "prisma": "^5.22.0", "puppeteer": "^22.12.0", "serverless": "^4.4.7", "serverless-offline": "^14.3.4", diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 039108f98..ad6faeefa 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - previewFeatures = ["fullTextSearch"] + previewFeatures = ["fullTextSearchPostgres", "typedSql"] binaryTargets = ["native", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } diff --git a/api/src/components/crosslink/service.ts b/api/src/components/crosslink/service.ts index 6e93906bb..799944198 100644 --- a/api/src/components/crosslink/service.ts +++ b/api/src/components/crosslink/service.ts @@ -1,4 +1,5 @@ import { Prisma } from '@prisma/client'; +// import { getPublicationCrosslinks } from '@prisma/client/sql'; import * as client from 'lib/client'; import * as I from 'interface'; @@ -189,215 +190,221 @@ export const getVote = (crosslinkId: string, userId: string) => } }); -export const getPublicationCrosslinks = async (publicationId: string, options?: I.GetPublicationCrosslinksOptions) => { - /** - * Raw SQL query alternative: - -PREPARE getPublicationCrosslinks (text) AS - SELECT - c.id, - c."publicationToId" AS linkedPublicationId, - pv.title AS linkedPublicationTitle, - c."createdBy", - c."createdAt", - c.score - FROM "Crosslink" AS c - JOIN "PublicationVersion" AS pv ON c."publicationToId" = pv."publicationId" - WHERE c."publicationFromId" = $1 AND pv."isLatestLiveVersion" - UNION - SELECT - c.id, - c."publicationFromId" AS linkedPublicationId, - pv.title AS linkedPublicationTitle, - c."createdBy", - c."createdAt", - c.score - FROM "Crosslink" AS c - JOIN "PublicationVersion" AS pv ON c."publicationFromId" = pv."publicationId" - WHERE "publicationToId" = $1 AND pv."isLatestLiveVersion"; - -EXECUTE getPublicationCrosslinks('publicationId'); - */ - const { order, search, limit, offset, userIdFilter } = options || {}; - const publicationInclude = { +type GetPublicationCrosslinksQueryResult = { + id: string; + linkedPublicationId: string; + createdBy: string; + createdAt: string; + score: number; + linkedPublicationLatestLiveVersionId: string; + linkedPublicationTitle: string; + linkedPublicationPublishedDate: string; + linkedPublicationAuthorId: string; + linkedPublicationAuthorFirstName: string; + linkedPublicationAuthorLastName: string; +}; + +type RelativeCrosslink = { + id: string; + score: number; + createdBy: string; + createdAt: string; + linkedPublication: Pick & { + latestLiveVersion: Pick & { + user: Pick; + }; + }; +}; +interface GetPublicationCrosslinksQueryOptions extends I.GetPublicationCrosslinksOptions { + excludedPublicationIds?: string[]; +} + +const getPublicationCrosslinksQuery = async ( + publicationId: string, + options?: GetPublicationCrosslinksQueryOptions +): Promise => { + const { order, search, limit, offset, userIdFilter, excludedPublicationIds } = options || {}; + + const conditionalFilterClauses: Prisma.Sql[] = []; + // Some conditional filter clauses have to be tailored depending whether we are getting + // crosslinks "from" or "to" the publication. + const conditionalFilterClausesFrom: Prisma.Sql[] = []; + const conditionalFilterClausesTo: Prisma.Sql[] = []; + + if (userIdFilter) { + conditionalFilterClauses.push(Prisma.sql`c."createdBy" = ${userIdFilter}`); + } + + if (search) { + conditionalFilterClauses.push(Prisma.sql`to_tsvector('english', pv.title) @@ to_tsquery('english', ${search})`); + } + + if (excludedPublicationIds) { + conditionalFilterClausesTo.push( + Prisma.sql`c."publicationToId" NOT IN (${Prisma.join(excludedPublicationIds)})` + ); + conditionalFilterClausesFrom.push( + Prisma.sql`c."publicationFromId" NOT IN (${Prisma.join(excludedPublicationIds)})` + ); + } + + const conditionalWhereTo = + conditionalFilterClauses.length || conditionalFilterClausesTo.length + ? Prisma.sql`AND ${Prisma.join([...conditionalFilterClauses, ...conditionalFilterClausesTo], ' AND ')}` + : Prisma.empty; + const conditionalWhereFrom = + conditionalFilterClauses.length || conditionalFilterClausesFrom.length + ? Prisma.sql`AND ${Prisma.join([...conditionalFilterClauses, ...conditionalFilterClausesFrom], ' AND ')}` + : Prisma.empty; + + const crosslinks = await client.prisma.$queryRaw` + SELECT * FROM ( + SELECT + c.id, + c."publicationToId" AS "linkedPublicationId", + c."createdBy", + c."createdAt", + c.score, + pv.id AS "linkedPublicationLatestLiveVersionId", + pv.title AS "linkedPublicationTitle", + pv."publishedDate" AS "linkedPublicationPublishedDate", + pvu.id AS "linkedPublicationAuthorId", + pvu."firstName" AS "linkedPublicationAuthorFirstName", + pvu."lastName" AS "linkedPublicationAuthorLastName" + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationToId" = pv."versionOf" + JOIN "User" AS pvu ON pvu.id = pv."createdBy" + WHERE + c."publicationFromId" = ${publicationId} + AND pv."isLatestLiveVersion" + ${conditionalWhereTo} + UNION + SELECT + c.id, + c."publicationFromId" AS linkedPublicationId, + c."createdBy", + c."createdAt", + c.score, + pv.id AS "linkedPublicationLatestLiveVersionId", + pv.title AS linkedPublicationTitle, + pv."publishedDate" AS "linkedPublicationPublishedDate", + pvu.id AS "linkedPublicationAuthorId", + pvu."firstName" AS "linkedPublicationAuthorFirstName", + pvu."lastName" AS "linkedPublicationAuthorlastName" + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationFromId" = pv."versionOf" + JOIN "User" AS pvu ON pvu.id = pv."createdBy" + WHERE + c."publicationToId" = ${publicationId} + AND pv."isLatestLiveVersion" + ${conditionalWhereFrom} + ) AS crosslinks + --- Recency order is default and also used to order results with the same score when + --- sorting by relevance. + ORDER BY + (CASE WHEN ${order} = 'relevant' THEN score END) DESC, + "createdAt" DESC + LIMIT ${limit || 10} + OFFSET ${offset || 0}; + `; + + const versionIds = crosslinks.map((crosslink) => crosslink.linkedPublicationLatestLiveVersionId); + const coAuthors = await client.prisma.coAuthors.findMany({ + where: { + publicationVersionId: { + in: versionIds + } + }, select: { - id: true, - versions: { - where: { - isLatestLiveVersion: true - }, + publicationVersionId: true, + linkedUser: true, + user: { select: { - title: true, - publishedDate: true, - user: { - select: { - id: true, - firstName: true, - lastName: true - } - }, - coAuthors: { - select: { - linkedUser: true, - user: { - select: { - firstName: true, - lastName: true, - role: true - } - } - } - } + firstName: true, + lastName: true, + role: true } } } - }; + }); - // If a query term is supplied, match against the latest live title of the - // crosslinked publication, i.e. not the one whose publicationId was supplied. - const where: Prisma.CrosslinkWhereInput = { - ...(search - ? { - OR: [ - { - publicationFromId: publicationId, - publicationTo: { - versions: { - some: { - isLatestLiveVersion: true, - title: { - search: search + ':*' - } - } - } - } - }, - { - publicationToId: publicationId, - publicationFrom: { - versions: { - some: { - isLatestLiveVersion: true, - title: { - search: search + ':*' - } - } - } - } - } - ] - } - : { - OR: [ - { - publicationFromId: publicationId - }, - { - publicationToId: publicationId - } - ] - }), - ...(userIdFilter ? { createdBy: userIdFilter } : {}) - }; + // TODO: Confirm coauthors are returned properly (seed data doesn't have coauthors) + // TODO: Provide a total result count + + return crosslinks.map((rawCrosslink) => ({ + id: rawCrosslink.id, + linkedPublication: { + id: rawCrosslink.linkedPublicationId, + latestLiveVersion: { + title: rawCrosslink.linkedPublicationTitle, + publishedDate: rawCrosslink.linkedPublicationPublishedDate, + user: { + id: rawCrosslink.linkedPublicationAuthorId, + firstName: rawCrosslink.linkedPublicationAuthorFirstName, + lastName: rawCrosslink.linkedPublicationAuthorLastName + }, + coAuthors: coAuthors + .filter( + (coAuthor) => + coAuthor.publicationVersionId === rawCrosslink.linkedPublicationLatestLiveVersionId + ) + .map((coAuthor) => { + const { publicationVersionId, ...rest } = coAuthor; + + return rest; + }) + } + }, + score: rawCrosslink.score, + createdBy: rawCrosslink.createdBy, + createdAt: rawCrosslink.createdAt + })); +}; - let rawCrosslinks; +export const getPublicationCrosslinks = async ( + publicationId: string, + options?: I.GetPublicationCrosslinksOptions +): Promise<{ + data: + | { + recent: RelativeCrosslink[]; + relevant: RelativeCrosslink[]; + } + | RelativeCrosslink[]; + metadata: I.SearchResultMeta; +}> => { + const { order, limit, offset } = options || {}; if (order === 'mix') { - const recent = await client.prisma.crosslink.findMany({ - where, - include: { - publicationFrom: publicationInclude, - publicationTo: publicationInclude - }, - orderBy: { - createdAt: 'desc' - }, - take: 2 + const recent = await getPublicationCrosslinksQuery(publicationId, { + ...options, + order: 'recent', + limit: 2 }); - const recentIds = recent.map((crosslink) => crosslink.id); - const relevant = await client.prisma.crosslink.findMany({ - where: { - ...where, - id: { - not: { - in: recentIds - } - } - }, - include: { - publicationFrom: publicationInclude, - publicationTo: publicationInclude - }, - orderBy: { - score: 'desc' - }, - take: 3 + console.log(recent); + const relevant = await getPublicationCrosslinksQuery(publicationId, { + ...options, + order: 'relevant', + limit: 3, + excludedPublicationIds: recent.map((crosslink) => crosslink.linkedPublication.id) }); - rawCrosslinks = [...recent, ...relevant]; - } else { - const orderBy: Prisma.CrosslinkOrderByWithRelationAndSearchRelevanceInput = - order === 'relevant' - ? { - score: 'desc' - } - : { - createdAt: 'desc' - }; - rawCrosslinks = await client.prisma.crosslink.findMany({ - where, - include: { - publicationFrom: publicationInclude, - publicationTo: publicationInclude - }, - orderBy, - take: limit, - skip: offset - }); - } - // Simplify data. - const crosslinks = rawCrosslinks.map((crosslink) => { - // Only return the other publication's details; we already know about the one whose ID was passed. - const linkedPublication = - crosslink.publicationFromId === publicationId ? crosslink.publicationTo : crosslink.publicationFrom; - const { versions, ...linkedPublicationRest } = linkedPublication; - const linkedPublicationWithSquashedVersion = { - ...linkedPublicationRest, - latestLiveVersion: versions[0] // Only the latest live version comes from the query. + return { + data: { + recent, + relevant + } }; + } else { + const crosslinks = await getPublicationCrosslinksQuery(publicationId, options); return { - id: crosslink.id, - linkedPublication: linkedPublicationWithSquashedVersion, - score: crosslink.score, - createdBy: crosslink.createdBy, - createdAt: crosslink.createdAt + data: crosslinks, + metadata: { + total: 0, + limit: limit || 10, + offset: offset || 0 + } }; - }); - - // Sort data. - const sortedCrosslinks = - order === 'mix' - ? crosslinks.length <= 2 - ? { - recent: crosslinks, - relevant: [] - } - : { - recent: [crosslinks[0], crosslinks[1]], - relevant: [...crosslinks.slice(2).sort((a, b) => b.score - a.score)] - } - : crosslinks; - - // Get total count. - const totalCrosslinks = await client.prisma.crosslink.count({ where }); - - return { - data: sortedCrosslinks, - metadata: { - total: totalCrosslinks, - limit, - offset - } - }; + } }; diff --git a/api/src/lib/interface.ts b/api/src/lib/interface.ts index 688edba94..a83f79cf3 100644 --- a/api/src/lib/interface.ts +++ b/api/src/lib/interface.ts @@ -82,6 +82,12 @@ export interface JSONResponse { export type Environment = 'int' | 'prod'; +export type SearchResultMeta = { + total: number; + limit: number; + offset: number; +}; + /** * @description Publications */ @@ -933,10 +939,7 @@ export interface TopicsFilters { exclude?: string; } -export interface TopicsPaginatedResults { - offset: number; - limit: number; - total: number; +export interface TopicsPaginatedResults extends SearchResultMeta { results: { id: string; title: string; diff --git a/ui/src/lib/interfaces.ts b/ui/src/lib/interfaces.ts index f565374a4..ff16c9b0f 100644 --- a/ui/src/lib/interfaces.ts +++ b/ui/src/lib/interfaces.ts @@ -559,7 +559,6 @@ export interface Crosslink { export interface RelativeCrosslink { id: string; linkedPublication: { - id: string; latestLiveVersion: Pick & { publishedDate: string; user: Pick; From f1780ed4d94cf8baaefd69006882a37080b3f2db Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 13 Dec 2024 10:54:53 +0000 Subject: [PATCH 3/9] provide total result count --- api/src/components/crosslink/service.ts | 81 +++++++++++++++++-------- ui/src/lib/interfaces.ts | 1 + 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/api/src/components/crosslink/service.ts b/api/src/components/crosslink/service.ts index 799944198..bfe618c7f 100644 --- a/api/src/components/crosslink/service.ts +++ b/api/src/components/crosslink/service.ts @@ -222,7 +222,10 @@ interface GetPublicationCrosslinksQueryOptions extends I.GetPublicationCrosslink const getPublicationCrosslinksQuery = async ( publicationId: string, options?: GetPublicationCrosslinksQueryOptions -): Promise => { +): Promise<{ + results: RelativeCrosslink[]; + total: number; +}> => { const { order, search, limit, offset, userIdFilter, excludedPublicationIds } = options || {}; const conditionalFilterClauses: Prisma.Sql[] = []; @@ -257,6 +260,26 @@ const getPublicationCrosslinksQuery = async ( ? Prisma.sql`AND ${Prisma.join([...conditionalFilterClauses, ...conditionalFilterClausesFrom], ' AND ')}` : Prisma.empty; + const crosslinkedToQueryWithoutSelect = Prisma.sql` + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationToId" = pv."versionOf" + JOIN "User" AS pvu ON pvu.id = pv."createdBy" + WHERE + c."publicationFromId" = ${publicationId} + AND pv."isLatestLiveVersion" + ${conditionalWhereTo} + `; + + const crosslinkedFromQueryWithoutSelect = Prisma.sql` + FROM "Crosslink" AS c + JOIN "PublicationVersion" AS pv ON c."publicationFromId" = pv."versionOf" + JOIN "User" AS pvu ON pvu.id = pv."createdBy" + WHERE + c."publicationToId" = ${publicationId} + AND pv."isLatestLiveVersion" + ${conditionalWhereFrom} + `; + const crosslinks = await client.prisma.$queryRaw` SELECT * FROM ( SELECT @@ -271,13 +294,7 @@ const getPublicationCrosslinksQuery = async ( pvu.id AS "linkedPublicationAuthorId", pvu."firstName" AS "linkedPublicationAuthorFirstName", pvu."lastName" AS "linkedPublicationAuthorLastName" - FROM "Crosslink" AS c - JOIN "PublicationVersion" AS pv ON c."publicationToId" = pv."versionOf" - JOIN "User" AS pvu ON pvu.id = pv."createdBy" - WHERE - c."publicationFromId" = ${publicationId} - AND pv."isLatestLiveVersion" - ${conditionalWhereTo} + ${crosslinkedToQueryWithoutSelect} UNION SELECT c.id, @@ -291,13 +308,7 @@ const getPublicationCrosslinksQuery = async ( pvu.id AS "linkedPublicationAuthorId", pvu."firstName" AS "linkedPublicationAuthorFirstName", pvu."lastName" AS "linkedPublicationAuthorlastName" - FROM "Crosslink" AS c - JOIN "PublicationVersion" AS pv ON c."publicationFromId" = pv."versionOf" - JOIN "User" AS pvu ON pvu.id = pv."createdBy" - WHERE - c."publicationToId" = ${publicationId} - AND pv."isLatestLiveVersion" - ${conditionalWhereFrom} + ${crosslinkedFromQueryWithoutSelect} ) AS crosslinks --- Recency order is default and also used to order results with the same score when --- sorting by relevance. @@ -308,6 +319,19 @@ const getPublicationCrosslinksQuery = async ( OFFSET ${offset || 0}; `; + const totalQueryResults = await client.prisma.$queryRaw<[{ count: number }]>` + SELECT count(*) FROM ( + SELECT c.id + ${crosslinkedToQueryWithoutSelect} + UNION + SELECT c.id + ${crosslinkedFromQueryWithoutSelect} + ) AS crosslinks; + `; + // This looks bad but the count comes back as a "bigint" type. + // Treating it as a number right away causes errors in JSON serialization, so have to convert it properly. + const total = Number.parseInt(String(totalQueryResults[0].count)); + const versionIds = crosslinks.map((crosslink) => crosslink.linkedPublicationLatestLiveVersionId); const coAuthors = await client.prisma.coAuthors.findMany({ where: { @@ -328,10 +352,8 @@ const getPublicationCrosslinksQuery = async ( } }); - // TODO: Confirm coauthors are returned properly (seed data doesn't have coauthors) - // TODO: Provide a total result count - - return crosslinks.map((rawCrosslink) => ({ + // Map SQL results to return format. + const results = crosslinks.map((rawCrosslink) => ({ id: rawCrosslink.id, linkedPublication: { id: rawCrosslink.linkedPublicationId, @@ -359,6 +381,11 @@ const getPublicationCrosslinksQuery = async ( createdBy: rawCrosslink.createdBy, createdAt: rawCrosslink.createdAt })); + + return { + results, + total + }; }; export const getPublicationCrosslinks = async ( @@ -381,27 +408,31 @@ export const getPublicationCrosslinks = async ( order: 'recent', limit: 2 }); - console.log(recent); const relevant = await getPublicationCrosslinksQuery(publicationId, { ...options, order: 'relevant', limit: 3, - excludedPublicationIds: recent.map((crosslink) => crosslink.linkedPublication.id) + excludedPublicationIds: recent.results.map((crosslink) => crosslink.linkedPublication.id) }); return { data: { - recent, - relevant + recent: recent.results, + relevant: relevant.results + }, + metadata: { + total: recent.total, // Because IDs are excluded from the relevant query, use the count from the recent query. + limit: 5, + offset: 0 } }; } else { const crosslinks = await getPublicationCrosslinksQuery(publicationId, options); return { - data: crosslinks, + data: crosslinks.results, metadata: { - total: 0, + total: crosslinks.total, limit: limit || 10, offset: offset || 0 } diff --git a/ui/src/lib/interfaces.ts b/ui/src/lib/interfaces.ts index 5622e879a..c44b4cc2d 100644 --- a/ui/src/lib/interfaces.ts +++ b/ui/src/lib/interfaces.ts @@ -546,6 +546,7 @@ export interface Crosslink { export interface RelativeCrosslink { id: string; linkedPublication: { + id: string; latestLiveVersion: Pick & { publishedDate: string; user: Pick; From 5cd6de2692c06549595fe3ea79bb40902a679a55 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 13 Dec 2024 11:41:04 +0000 Subject: [PATCH 4/9] remove typed sql feature and put prisma version back --- api/package-lock.json | 4 ++-- api/package.json | 4 ++-- api/prisma/schema.prisma | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 830c8b519..f656f7153 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -14,7 +14,7 @@ "@middy/http-json-body-parser": "^4.7.0", "@opensearch-project/opensearch": "^2.5.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.22.0", + "@prisma/client": "^5.11.0", "@sparticuz/chromium": "^119.0.2", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -57,7 +57,7 @@ "jest": "^29.7.0", "lint-staged": "^13.3.0", "prettier": "^2.8.8", - "prisma": "^5.22.0", + "prisma": "^5.11.0", "puppeteer": "^22.12.0", "serverless": "^4.4.7", "serverless-offline": "^14.3.4", diff --git a/api/package.json b/api/package.json index 1935eca9d..3fbdfcae1 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,7 @@ "@middy/http-json-body-parser": "^4.7.0", "@opensearch-project/opensearch": "^2.5.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.22.0", + "@prisma/client": "^5.11.0", "@sparticuz/chromium": "^119.0.2", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -89,7 +89,7 @@ "jest": "^29.7.0", "lint-staged": "^13.3.0", "prettier": "^2.8.8", - "prisma": "^5.22.0", + "prisma": "^5.11.0", "puppeteer": "^22.12.0", "serverless": "^4.4.7", "serverless-offline": "^14.3.4", diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index ad6faeefa..59b00f8dd 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - previewFeatures = ["fullTextSearchPostgres", "typedSql"] + previewFeatures = ["fullTextSearchPostgres"] binaryTargets = ["native", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } From 046d01934f0de057641d936bdbe07e18ed4dfb59 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 13 Dec 2024 11:49:33 +0000 Subject: [PATCH 5/9] rename preview feature back to how it was in old version --- api/prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 59b00f8dd..039108f98 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - previewFeatures = ["fullTextSearchPostgres"] + previewFeatures = ["fullTextSearch"] binaryTargets = ["native", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } From 1340f5be79a0a4f8059d3e07cb136a6f78b9f1aa Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 13 Dec 2024 11:49:51 +0000 Subject: [PATCH 6/9] revert package-lock to same as main --- api/package-lock.json | 74 +++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index f656f7153..d64d099cd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10097,11 +10097,10 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz", + "integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==", "hasInstallScript": true, - "license": "Apache-2.0", "engines": { "node": ">=16.13" }, @@ -10115,53 +10114,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true, - "license": "Apache-2.0" + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz", + "integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==", + "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz", + "integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==", "devOptional": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/fetch-engine": "5.11.0", + "@prisma/get-platform": "5.11.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, - "license": "Apache-2.0" + "version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz", + "integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==", + "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz", + "integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/get-platform": "5.11.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz", + "integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "5.11.0" } }, "node_modules/@puppeteer/browsers": { @@ -19670,23 +19664,19 @@ } }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz", + "integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==", "devOptional": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/engines": "5.11.0" }, "bin": { "prisma": "build/index.js" }, "engines": { "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" } }, "node_modules/process-nextick-args": { From 909a0a2681795a60a05f6c3618b538a08ad5ec9e Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Fri, 13 Dec 2024 11:50:00 +0000 Subject: [PATCH 7/9] remove comment --- api/src/components/crosslink/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/components/crosslink/service.ts b/api/src/components/crosslink/service.ts index bfe618c7f..e3b4d05fd 100644 --- a/api/src/components/crosslink/service.ts +++ b/api/src/components/crosslink/service.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client'; -// import { getPublicationCrosslinks } from '@prisma/client/sql'; import * as client from 'lib/client'; import * as I from 'interface'; From 70e1c695b8131c21a2b19125ec9e7c9c6d74e965 Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Mon, 16 Dec 2024 09:02:22 +0000 Subject: [PATCH 8/9] check array length in condition --- api/src/components/crosslink/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/components/crosslink/service.ts b/api/src/components/crosslink/service.ts index e3b4d05fd..d0649a49e 100644 --- a/api/src/components/crosslink/service.ts +++ b/api/src/components/crosslink/service.ts @@ -241,7 +241,7 @@ const getPublicationCrosslinksQuery = async ( conditionalFilterClauses.push(Prisma.sql`to_tsvector('english', pv.title) @@ to_tsquery('english', ${search})`); } - if (excludedPublicationIds) { + if (excludedPublicationIds?.length) { conditionalFilterClausesTo.push( Prisma.sql`c."publicationToId" NOT IN (${Prisma.join(excludedPublicationIds)})` ); From 577aff271229076bc4f04812e9142fd3c08179ca Mon Sep 17 00:00:00 2001 From: Finlay Birnie Date: Mon, 16 Dec 2024 09:14:50 +0000 Subject: [PATCH 9/9] get around prisma not finding openssl issue --- api/Dockerfile | 5 ++++- api/prisma/schema.prisma | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 06c4a12b4..270aa92d7 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -7,7 +7,10 @@ RUN apk add \ curl \ gnupg \ lsb-release \ - wget + wget \ + openssl \ + openssl-dev \ + libc6-compat # Dockerize is needed to sync containers startup ENV DOCKERIZE_VERSION v0.6.1 diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 039108f98..f04c1057b 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -1,7 +1,7 @@ generator client { provider = "prisma-client-js" previewFeatures = ["fullTextSearch"] - binaryTargets = ["native", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] + binaryTargets = ["native", "linux-musl", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"] } datasource db {