Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OC-1014: New endpoint to fetch user's drafts #778

Merged
merged 3 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion api/src/components/user/__tests__/getUserPublications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Prisma } from '@prisma/client';
import * as testUtils from 'lib/testUtils';
import * as userService from 'user/service';

type UserPublications = Prisma.PromiseReturnType<typeof userService.getPublications>;

describe("Get a given user's publications", () => {
beforeAll(async () => {
await testUtils.clearDB();
Expand Down Expand Up @@ -58,7 +60,6 @@ describe("Get a given user's publications", () => {

test('Results can be filtered by a query term', async () => {
const queryTerm = 'interpretation';
type UserPublications = Prisma.PromiseReturnType<typeof userService.getPublications>;
const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ query: queryTerm });

expect(publications.status).toEqual(200);
Expand All @@ -71,4 +72,100 @@ describe("Get a given user's publications", () => {
)
).toEqual(true);
});

test('Results can be filtered to publications having at least one version currently in a given status', async () => {
const publications = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, versionStatus: 'DRAFT' });

expect(publications.status).toEqual(200);
expect(publications.body.data.length).toEqual(10); // a full page
expect(
(publications.body as UserPublications).data.every((publication) =>
publication.versions.some((version) => version.currentStatus === 'DRAFT')
)
).toEqual(true);
});

test('versionStatus param cannot be used if initialDraftsOnly param is true', async () => {
const publications = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, versionStatus: 'DRAFT', initialDraftsOnly: true });

expect(publications.status).toEqual(400);
expect(publications.body.message).toBe(
'The "versionStatus" query parameter cannot be used when "initialDraftsOnly" is set to true.'
);
});

test('versionStatus param can be used if initialDraftsOnly param is false', async () => {
const publications = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, versionStatus: 'DRAFT', initialDraftsOnly: false });

expect(publications.status).toEqual(200);
});

test('Only initial drafts are returned if initialDraftsOnly param is true', async () => {
const publications = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, initialDraftsOnly: true });

expect(publications.status).toEqual(200);
expect(publications.body.data.length).toBeGreaterThan(0);
// Every version on every publication should be a draft, and the only version.
expect(
(publications.body as UserPublications).data.every(
(publication) =>
publication.versions.length === 1 &&
publication.versions[0].currentStatus === 'DRAFT' &&
publication.versions[0].versionNumber === 1
)
).toEqual(true);
});

test('Publications with their ID provided to "exclude" param can be excluded from results', async () => {
const firstRequest = await testUtils.agent.get('/users/test-user-1/publications').query({ apiKey: 123456789 });

expect(firstRequest.status).toEqual(200);
const firstTotal = firstRequest.body.metadata.total;
expect(firstTotal).toBeGreaterThan(0);

const publicationIdToExclude = firstRequest.body.data[0].id;

const secondRequest = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, exclude: publicationIdToExclude });

expect(secondRequest.status).toEqual(200);
expect(secondRequest.body.metadata.total).toEqual(firstTotal - 1);
expect(
(secondRequest.body as UserPublications).data.every(
(publication) => publication.id !== publicationIdToExclude
)
).toEqual(true);
});

test('Multiple, comma-separated publications can be excluded from results', async () => {
const firstRequest = await testUtils.agent.get('/users/test-user-1/publications').query({ apiKey: 123456789 });

expect(firstRequest.status).toEqual(200);
const firstTotal = firstRequest.body.metadata.total;
expect(firstTotal).toBeGreaterThan(1);

const publicationIdsToExclude = [firstRequest.body.data[0].id, firstRequest.body.data[1].id];
const exclusionString = publicationIdsToExclude.join(',');

const secondRequest = await testUtils.agent
.get('/users/test-user-1/publications')
.query({ apiKey: 123456789, exclude: exclusionString });

expect(secondRequest.status).toEqual(200);
expect(secondRequest.body.metadata.total).toEqual(firstTotal - 2);
expect(
(secondRequest.body as UserPublications).data.every(
(publication) => !publicationIdsToExclude.includes(publication.id)
)
).toEqual(true);
});
});
8 changes: 7 additions & 1 deletion api/src/components/user/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ export const getPublications = async (
const versionStatus = event.queryStringParameters?.versionStatus;
const versionStatusArray = versionStatus ? versionStatus.split(',') : [];

if (event.queryStringParameters.initialDraftsOnly && versionStatusArray.length) {
return response.json(400, {
message: 'The "versionStatus" query parameter cannot be used when "initialDraftsOnly" is set to true.'
});
}

if (versionStatusArray.length && versionStatusArray.some((status) => !I.PublicationStatusEnum[status])) {
return response.json(400, {
message: "Invalid version status provided. Valid values include 'DRAFT', 'LIVE', 'LOCKED"
message: "Invalid version status provided. Valid values include 'DRAFT', 'LIVE', and 'LOCKED'."
});
}

Expand Down
9 changes: 9 additions & 0 deletions api/src/components/user/schema/getPublications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const getPublicationsSchema: I.JSONSchemaType<I.UserPublicationsFilters> = {
versionStatus: {
type: 'string',
nullable: true
},
initialDraftsOnly: {
type: 'boolean',
default: false,
nullable: true
},
exclude: {
type: 'string',
nullable: true
}
},
required: []
Expand Down
73 changes: 43 additions & 30 deletions api/src/components/user/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,19 @@ export const getPublications = async (
isAccountOwner: boolean,
versionStatusArray?: I.PublicationStatusEnum[]
) => {
const { offset, limit } = params;
const { offset, limit, initialDraftsOnly } = params;

const user = await client.prisma.user.findUnique({
where: {
id
}
});

// Get publications where:
const where: Prisma.PublicationWhereInput = {
const requestingNonLiveVersions =
versionStatusArray?.some((status) => status === 'DRAFT' || status === 'LOCKED') || initialDraftsOnly;

// WHERE clauses related to the user's authorship of a publication.
const authorshipFilter: Prisma.PublicationWhereInput = {
OR: [
// User is corresponding author on at least one version, or
{
Expand All @@ -214,10 +217,8 @@ export const getPublications = async (
}
},
// They are an unconfirmed coauthor on at least one version
// (if the user is requesting their own publications and including DRAFT/LOCKED ones)
...(isAccountOwner &&
user?.email &&
versionStatusArray?.some((status) => status === 'DRAFT' || status === 'LOCKED')
// (if the user is requesting their own publications and including non-live ones)
...(isAccountOwner && user?.email && requestingNonLiveVersions
? [
{
versions: {
Expand All @@ -234,36 +235,48 @@ export const getPublications = async (
}
]
: [])
],
// And, if the user is the account owner and filters have been provided,
// where there is a version with current status matching the given filters
...(isAccountOwner
? versionStatusArray
? {
versions: {
some: {
currentStatus: {
in: versionStatusArray
}
}
}
}
: {}
: // But if the user is not the owner, get only publications that have a published version
{ versions: { some: { isLatestLiveVersion: true } } }),
// And, if a query is supplied, where the query matches the latest live title.
...(params.query
]
};

// WHERE clauses related to the requested publication status of publications.
const versionStatusFilter: Prisma.PublicationWhereInput =
// If the requester is not the account owner, get only publications that have a published version.
!isAccountOwner
? { versions: { some: { isLatestLiveVersion: true } } }
: // Otherwise...
// If only initial drafts are requested, return only these.
initialDraftsOnly
? { versions: { every: { currentStatus: 'DRAFT', versionNumber: 1 } } }
: // If another version status filter has been supplied, get publications with at least one version with a current status matching the given filters.
versionStatusArray
? {
versions: {
some: {
isLatestLiveVersion: true,
title: {
search: Helpers.sanitizeSearchQuery(params.query)
currentStatus: {
in: versionStatusArray
}
}
}
}
: {})
: {};

// Get publications where:
const where: Prisma.PublicationWhereInput = {
...authorshipFilter,
...versionStatusFilter,
// If a query is supplied, the query matches the latest live title.
...(params.query && {
versions: {
some: {
isLatestLiveVersion: true,
title: {
search: Helpers.sanitizeSearchQuery(params.query)
}
}
}
}),
// If a publication is to be excluded, it is not included in the results.
...(params.exclude && { id: { notIn: params.exclude.split(',') } })
};

const userPublications = await client.prisma.publication.findMany({
Expand Down
2 changes: 2 additions & 0 deletions api/src/lib/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,8 @@ export interface UserPublicationsFilters {
limit: number;
query?: string;
versionStatus?: string;
initialDraftsOnly?: boolean;
exclude?: string;
}

export interface SendApprovalReminderPathParams {
Expand Down