Skip to content

Commit

Permalink
[ASAP-629] - add users public api (#4372)
Browse files Browse the repository at this point in the history
* update query & data provider

* update user model

* update tests & mocks

* fix failing tests

* added tests

* coverage

* updates from review

* return related fields together
  • Loading branch information
AkosuaA committed Sep 17, 2024
1 parent d5e5665 commit 7dc48fb
Show file tree
Hide file tree
Showing 17 changed files with 1,258 additions and 25 deletions.
56 changes: 54 additions & 2 deletions apps/crn-server/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { GenericError, NotFoundError } from '@asap-hub/errors';
import Boom from '@hapi/boom';
import {
FetchUsersOptions,
ListPublicUserResponse,
ListUserResponse,
PublicUserDataObject,
PublicUserResponse,
UserDataObject,
UserResponse,
UserUpdateDataObject,
Expand Down Expand Up @@ -70,8 +73,23 @@ export default class UserController {
};
}

async fetchById(id: string): Promise<UserResponse> {
const user = await this.userDataProvider.fetchById(id);
async fetchPublicUsers(
options: FetchUsersOptions,
): Promise<ListPublicUserResponse> {
const { total, items } =
await this.userDataProvider.fetchPublicUsers(options);

return {
total,
items: items.map(parsePublicUserToResponse),
};
}

async fetchById(
id: string,
publicUser: boolean = false,
): Promise<UserResponse> {
const user = await this.userDataProvider.fetchById(id, publicUser);

if (!user) {
throw new NotFoundError(undefined, `user with id ${id} not found`);
Expand Down Expand Up @@ -262,3 +280,37 @@ export const parseUserToResponse = ({
onboarded,
};
};

export const parsePublicUserToResponse = ({
...user
}: PublicUserDataObject): PublicUserResponse => ({
avatarUrl: user.avatarUrl,
biography: user.biography,
city: user.city,
country: user.country,
createdDate: user.createdDate,
lastModifiedDate: user.lastModifiedDate,
degree: user.degree,
firstName: user.firstName,
lastName: user.lastName,
id: user.id,
institution: user.institution,
interestGroups: user.interestGroups.map((ig) => ({
name: ig.name,
})),
labs: user.labs,
researchTheme: user.researchTheme,
researchOutputs: user.researchOutputs || [],
tags: user.tags?.map((tag) => tag.name) || [],
teams: user.teams.map((team) => ({
displayName: team.displayName || '',
role: team.role,
})),
workingGroups: user.workingGroups
.filter((wg) => wg.active)
.map((wg) => ({
name: wg.name,
role: wg.role,
})),
...user.social,
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isUserDegree,
isUserRole,
LabResponse,
ListPublicUserDataObject,
ListUserDataObject,
OrcidWork,
UserDataObject,
Expand All @@ -19,6 +20,8 @@ import {

import {
Environment,
FetchPublicUsersQuery,
FetchPublicUsersQueryVariables,
FetchUserByIdQuery,
FetchUserByIdQueryVariables,
FetchUsersByLabIdQuery,
Expand All @@ -27,6 +30,7 @@ import {
FetchUsersByTeamIdQueryVariables,
FetchUsersQuery,
FetchUsersQueryVariables,
FETCH_PUBLIC_USERS,
FETCH_USERS,
FETCH_USERS_BY_LAB_ID,
FETCH_USERS_BY_TEAM_ID,
Expand Down Expand Up @@ -55,6 +59,16 @@ export type LabsCollection = UserItem['labsCollection'];

export type TeamsCollection = UserItem['teamsCollection'];

export type ResearchOutputsCollection = NonNullable<
UserItem['linkedFrom']
>['researchOutputsCollection'];

export type ResearchOutputItem = NonNullable<
NonNullable<
NonNullable<UserItem['linkedFrom']>['researchOutputsCollection']
>['items'][number]
>;

export type TeamMembership = NonNullable<
NonNullable<UserItem['teamsCollection']>['items'][number]
>;
Expand Down Expand Up @@ -96,15 +110,18 @@ export class UserContentfulDataProvider implements UserDataProvider {
private getRestClient: () => Promise<Environment>,
) {}

private fetchUserById(id: string) {
private fetchUserById(id: string, publicUser: boolean = false) {
return this.contentfulClient.request<
FetchUserByIdQuery,
FetchUserByIdQueryVariables
>(FETCH_USER_BY_ID, { id });
>(FETCH_USER_BY_ID, { id, publicUser });
}

async fetchById(id: string): Promise<UserDataObject | null> {
const { users } = await this.fetchUserById(id);
async fetchById(
id: string,
publicUser: boolean = false,
): Promise<UserDataObject | null> {
const { users } = await this.fetchUserById(id, publicUser);

if (!users) {
return null;
Expand All @@ -124,6 +141,19 @@ export class UserContentfulDataProvider implements UserDataProvider {
};
}

async fetchPublicUsers(
options: FetchUsersOptions,
): Promise<ListPublicUserDataObject> {
const result = await this.fetchPublicUsersData(options);

return {
total: result?.total,
items: result?.items
.filter((user): user is UserItem => user !== null)
.map(parseContentfulGraphQlUsers),
};
}

private async fetchUsers(options: FetchUsersOptions) {
const { take = 8, skip = 0 } = options;

Expand Down Expand Up @@ -187,6 +217,23 @@ export class UserContentfulDataProvider implements UserDataProvider {
return usersCollection || { total: 0, items: [] };
}

private async fetchPublicUsersData(options: FetchUsersOptions) {
const { take = 8, skip = 0 } = options;

const where: UsersFilter = generateFetchQueryFilter(options);

const { usersCollection } = await this.contentfulClient.request<
FetchPublicUsersQuery,
FetchPublicUsersQueryVariables
>(FETCH_PUBLIC_USERS, {
limit: take,
skip,
where,
order: [UsersOrder.LastNameAsc],
});
return usersCollection || { total: 0, items: [] };
}

async create(): Promise<string> {
throw new Error('Method not implemented.');
}
Expand Down Expand Up @@ -280,6 +327,7 @@ export const parseContentfulGraphQlUsers = (item: UserItem): UserDataObject => {
[],
);

const userId = item.sys.id;
const questions = normaliseArray(item.questions);
const connections = normaliseArray(item.connections);

Expand Down Expand Up @@ -318,8 +366,13 @@ export const parseContentfulGraphQlUsers = (item: UserItem): UserDataObject => {
...parseLeadersToInterestGroups(interestGroupLeadersCollection),
]);

const researchOutputs = parseResearchOutputsCollection(
item?.linkedFrom?.researchOutputsCollection,
userId,
);

return {
id: item.sys.id,
id: userId,
activeCampaignId: item.activeCampaignId || undefined,
membershipStatus: [
item.alumniSinceDate
Expand Down Expand Up @@ -352,6 +405,7 @@ export const parseContentfulGraphQlUsers = (item: UserItem): UserDataObject => {
alumniSinceDate: item.alumniSinceDate ?? undefined,
reachOut: item.reachOut ?? undefined,
researchInterests: item.researchInterests ?? undefined,
researchOutputs,
responsibilities: item.responsibilities ?? undefined,
expertiseAndResourceDescription:
item.expertiseAndResourceDescription ?? undefined,
Expand Down Expand Up @@ -523,6 +577,28 @@ export const parseTeamsCollection = (
[],
);

export const parseResearchOutputsCollection = (
researchOutputsCollection: ResearchOutputsCollection,
userId: string,
): string[] =>
(researchOutputsCollection?.items || []).reduce(
(
userResearchOutputs: string[],
researchOutput: ResearchOutputItem | null,
): string[] => {
const isAuthor = researchOutput?.authorsCollection?.items.some(
(author) => author?.__typename === 'Users' && author.sys.id === userId,
);

if (isAuthor && researchOutput?.sys.id) {
return [...userResearchOutputs, researchOutput.sys.id];
}

return userResearchOutputs;
},
[],
);

export const parseToWorkingGroups = (
users: (GroupMemberItem | GroupLeaderItem)[],
isAlumni: boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DataProvider,
FetchUsersOptions,
ListPublicUserDataObject,
UserCreateDataObject,
UserDataObject,
UserListItemDataObject,
Expand All @@ -15,4 +16,12 @@ export type UserDataProvider = DataProvider<
null,
UserUpdateDataObject,
{ suppressConflict?: boolean; polling?: boolean }
>;
> & {
fetchPublicUsers: (
options: FetchUsersOptions,
) => Promise<ListPublicUserDataObject>;
fetchById: (
id: string,
publicUser: boolean,
) => Promise<UserDataObject | null>;
};
31 changes: 30 additions & 1 deletion apps/crn-server/src/publicApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ import {
contentfulSpaceId,
} from './config';
import ResearchOutputController from './controllers/research-output.controller';
import UserController from './controllers/user.controller';
import { AssetContentfulDataProvider } from './data-providers/contentful/asset.data-provider';
import { ExternalAuthorContentfulDataProvider } from './data-providers/contentful/external-author.data-provider';
import { GenerativeContentDataProvider } from './data-providers/contentful/generative-content.data-provider';
import { ResearchOutputContentfulDataProvider } from './data-providers/contentful/research-output.data-provider';
import { ResearchTagContentfulDataProvider } from './data-providers/contentful/research-tag.data-provider';
import { UserContentfulDataProvider } from './data-providers/contentful/user.data-provider';
import {
ResearchOutputDataProvider,
ResearchTagDataProvider,
UserDataProvider,
} from './data-providers/types';
import { ExternalAuthorDataProvider } from './data-providers/types/external-authors.data-provider.types';
import { getContentfulRestClientFactory } from './dependencies/clients.dependencies';
import { researchOutputRouteFactory } from './routes/public/research-output.route';
import { userRouteFactory } from './routes/public/user.route';
import pinoLogger from './utils/logger';

export const publicAppFactory = (
Expand Down Expand Up @@ -86,6 +91,17 @@ export const publicAppFactory = (

const generativeContentDataProvider = new GenerativeContentDataProvider();

const userDataProvider =
dependencies.userDataProvider ||
new UserContentfulDataProvider(
contentfulGraphQLClient,
getContentfulRestClientFactory,
);

const assetDataProvider = new AssetContentfulDataProvider(
getContentfulRestClientFactory,
);

const researchOutputController =
dependencies.researchOutputController ||
new ResearchOutputController(
Expand All @@ -94,6 +110,15 @@ export const publicAppFactory = (
externalAuthorDataProvider,
generativeContentDataProvider,
);

const userController =
dependencies.userController ||
new UserController(
userDataProvider,
assetDataProvider,
researchTagDataProvider,
);

const basicRoutes = Router();

// add healthcheck route
Expand All @@ -105,8 +130,10 @@ export const publicAppFactory = (
researchOutputController,
);

const userRoutes = userRouteFactory(userController);

// add routes
app.use('/public', [basicRoutes, researchOutputRoutes]);
app.use('/public', [basicRoutes, researchOutputRoutes, userRoutes]);

// Catch all
app.get('/public/*', async (_req, res) => {
Expand Down Expand Up @@ -134,6 +161,8 @@ type PublicAppDependencies = {
researchOutputController?: ResearchOutputController;
researchOutputDataProvider?: ResearchOutputDataProvider;
researchTagDataProvider?: ResearchTagDataProvider;
userDataProvider?: UserDataProvider;
userController?: UserController;
sentryErrorHandler?: typeof Sentry.Handlers.errorHandler;
sentryRequestHandler?: typeof Sentry.Handlers.requestHandler;
sentryTransactionIdHandler?: RequestHandler;
Expand Down
Loading

0 comments on commit 7dc48fb

Please sign in to comment.