Skip to content

Commit

Permalink
fix: response schema + returned data
Browse files Browse the repository at this point in the history
  • Loading branch information
ojeytonwilliams committed May 16, 2024
1 parent 495034b commit 9295054
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 60 deletions.
8 changes: 5 additions & 3 deletions api/src/routes/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ const testUserData: Prisma.userCreateInput = {
],
savedChallenges: [
{
id: 'abc123',
// TODO: figure out why, when this was a short id, was it that only the get-public-profile
// tests failed, not the get-session-user tests.
id: 'a6b0bb188d873cb2c8729495',
lastSavedDate: 123,
files: [
{
Expand Down Expand Up @@ -1241,9 +1243,9 @@ Thanks and regards,
// TODO(Post-MVP, maybe): return completedSurveys?
..._.omit(publicUserData, 'completedSurveys'),
username: publicUsername,
id: testUser.id,
joinDate: new ObjectId(testUser.id).getTimestamp().toISOString(),
profileUI: unlockedUserProfileUI
profileUI: unlockedUserProfileUI,
isDonating: null
};

expect(response.body).toStrictEqual({
Expand Down
93 changes: 48 additions & 45 deletions api/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
import { ObjectId } from 'mongodb';
import { omit } from 'lodash';
import _ from 'lodash';

import * as schemas from '../schemas';
// Loopback creates a 64 character string for the user id, this customizes
// nanoid to do the same. Any unique key _should_ be fine, though.
import { customNanoid } from '../utils/ids';
import {
normalizeChallenges,
normalizeFlags,
normalizeProfileUI,
normalizeTwitter,
removeNulls,
Expand Down Expand Up @@ -648,27 +649,29 @@ export const userPublicGetRoutes: FastifyPluginCallbackTypebox = (
// ones.
});

console.log('user', user);

if (!user) {
void reply.code(404);
return reply.send({});
}

const publicUser = omit(user, [
const flags = _.pick<typeof user, NullableFlag>(user, nullableFlags);
const rest = _.omit<typeof user, NullableFlag>(user, nullableFlags);

const publicUser = _.omit(rest, [
'currentChallengeId',
'email',
'emailVerified',
'sendQuincyEmail',
'theme',
'keyboardShortcuts',
// keyboardShortcuts is included in flags.
// 'keyboardShortcuts',
'acceptedPrivacyTerms',
'progressTimestamps',
'unsubscribeId',
'donationEmails',
'externalId',
'usernameDisplay',
'isBanned' // TODO: should this be omitted?
'isBanned'
]);

const normalizedProfileUI = normalizeProfileUI(user.profileUI);
Expand All @@ -693,47 +696,47 @@ export const userPublicGetRoutes: FastifyPluginCallbackTypebox = (
const normalizedChallenges = normalizedProfileUI.showTimeLine
? normalizeChallenges(user.completedChallenges)
: [];

const returnedUser = {
[user.username]: {
...removeNulls(publicUser),
...normalizeFlags(flags),
about: normalizedProfileUI.showAbout ? user.about : '',
calendar: normalizedProfileUI.showHeatMap
? getCalendar(
user.progressTimestamps as ProgressTimestamp[] | null
)
: {},
completedChallenges: normalizedProfileUI.showCerts
? normalizedChallenges
: normalizedChallenges.filter(

Check notice on line 712 in api/src/routes/user.ts

View check run for this annotation

codefactor.io / CodeFactor

api/src/routes/user.ts#L644-L712

Complex Method
({ challengeType }) => challengeType !== challengeTypes.step // AKA certifications
),
isDonating:
normalizedProfileUI.showDonation && user.isDonating ? true : null,
joinDate: normalizedProfileUI.showAbout
? new ObjectId(user.id).getTimestamp().toISOString()
: '',
location:
normalizedProfileUI.showLocation && user.location
? user.location
: '',
name: normalizedProfileUI.showName && user.name ? user.name : '',
points: normalizedProfileUI.showPoints
? getPoints(user.progressTimestamps as ProgressTimestamp[] | null)
: 0,
portfolio: normalizedProfileUI.showPortfolio ? user.portfolio : [],
profileUI: normalizedProfileUI,
// TODO: should this always be returned? Shouldn't some privacy
// setting control it? Same applies to website, githubProfile,
// and linkedin.
twitter: normalizeTwitter(user.twitter),
yearsTopContributor: user.yearsTopContributor
}
};
return reply.send({
entities: {
user: {
[user.username]: {
...removeNulls(publicUser),
about: normalizedProfileUI.showAbout ? user.about : '',
calendar: normalizedProfileUI.showHeatMap
? getCalendar(
user.progressTimestamps as ProgressTimestamp[] | null
)
: {},
completedChallenges: normalizedProfileUI.showCerts
? normalizedChallenges
: normalizedChallenges.filter(
({ challengeType }) =>
challengeType !== challengeTypes.step // AKA certifications
),
isDonating: normalizedProfileUI.showDonation
? user.isDonating
: null,
joinDate: normalizedProfileUI.showAbout
? new ObjectId(user.id).getTimestamp().toISOString()
: '',
location: normalizedProfileUI.showLocation ? user.location : '',
name: normalizedProfileUI.showName ? user.name : '',
points: normalizedProfileUI.showPoints
? getPoints(
user.progressTimestamps as ProgressTimestamp[] | null
)
: 0,
portfolio: normalizedProfileUI.showPortfolio
? user.portfolio
: [],
profileUI: normalizedProfileUI,
// TODO: should this always be returned? Shouldn't some privacy
// setting control it? Same applies to website, githubProfile,
// and linkedin.
twitter: normalizeTwitter(user.twitter),
yearsTopContributor: user.yearsTopContributor
}
}
user: returnedUser
},
result: user.username
});
Expand Down
114 changes: 102 additions & 12 deletions api/src/schemas/api/users/get-public-profile.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,118 @@
import { Type } from '@fastify/type-provider-typebox';
import { profileUI } from '../../types';
import { profileUI, saveChallengeBody, examResults } from '../../types';

export const getPublicProfile = {
querystring: Type.Object({
username: Type.String({ minLength: 1 })
}),
response: {
200: Type.Union([
Type.Object({
entities: Type.Object({
user: Type.Record(
Type.String(),
200: Type.Object({
entities: Type.Object({
user: Type.Record(
Type.String(),
Type.Union([
Type.Object({
isLocked: Type.Boolean(),
profileUI,
username: Type.String()
}),
Type.Object({
about: Type.String(),
calendar: Type.Record(Type.Number(), Type.Literal(1)),
completedChallenges: Type.Array(
Type.Object({
id: Type.String(),
completedDate: Type.Number(),
solution: Type.Optional(Type.String()),
githubLink: Type.Optional(Type.String()),
challengeType: Type.Optional(Type.Number()),
files: Type.Array(
Type.Object({
contents: Type.String(),
key: Type.String(),
ext: Type.String(),
name: Type.String(),
path: Type.Optional(Type.String())
})
),
isManuallyApproved: Type.Optional(Type.Boolean())
})
),
completedExams: Type.Array(
Type.Object({
id: Type.String(),
completedDate: Type.Number(),
challengeType: Type.Optional(Type.Number()),
examResults
})
),
// TODO(Post-MVP): return completedSurveys? Presumably not, since why
// would this need to be public.
githubProfile: Type.Optional(Type.String()),
is2018DataVisCert: Type.Boolean(),
is2018FullStackCert: Type.Boolean(),
isApisMicroservicesCert: Type.Boolean(),
isBackEndCert: Type.Boolean(),
isCheater: Type.Boolean(),
isCollegeAlgebraPyCertV8: Type.Boolean(),
isDataAnalysisPyCertV7: Type.Boolean(),
isDataVisCert: Type.Boolean(),
// TODO(Post-MVP): isDonating should be boolean.
isDonating: Type.Union([Type.Boolean(), Type.Null()]),
isFoundationalCSharpCertV8: Type.Boolean(),
isFrontEndCert: Type.Boolean(),
isFrontEndLibsCert: Type.Boolean(),
isFullStackCert: Type.Boolean(),
isHonest: Type.Boolean(),
isInfosecCertV7: Type.Boolean(),
isInfosecQaCert: Type.Boolean(),
isJsAlgoDataStructCert: Type.Boolean(),
isJsAlgoDataStructCertV8: Type.Boolean(),
isMachineLearningPyCertV7: Type.Boolean(),
isQaCertV7: Type.Boolean(),
isRelationalDatabaseCertV8: Type.Boolean(),
isRespWebDesignCert: Type.Boolean(),
isSciCompPyCertV7: Type.Boolean(),
linkedin: Type.Optional(Type.String()),
location: Type.String(),
name: Type.String(),
partiallyCompletedChallenges: Type.Array(
Type.Object({
id: Type.String(),
completedDate: Type.Number()
})
),
picture: Type.String(), // TODO(Post-MVP): format as url/uri?
// TODO(Post-MVP): points should be a number
points: Type.Union([Type.Number(), Type.Null()]),
portfolio: Type.Array(
Type.Object({
description: Type.String(),
id: Type.String(),
image: Type.String(),
title: Type.String(),
url: Type.String()
})
),
profileUI,
twitter: Type.Optional(Type.String()),
website: Type.Optional(Type.String()),
yearsTopContributor: Type.Array(Type.String()), // TODO(Post-MVP): convert to number?
joinDate: Type.String(),
savedChallenges: Type.Array(
Type.Intersect([
saveChallengeBody,
Type.Object({ lastSavedDate: Type.Number() })
])
),
username: Type.String(),
msUsername: Type.Optional(Type.String())
})
)
}),
result: Type.String()
])
)
}),
// TODO: replace Any with real type.
Type.Object({ entities: Type.Any(), result: Type.String() })
]),
result: Type.String()
}),
// We can't simply have Type.Object({}), even though that's correct, because
// TypeScript will then accept all responses (since every object can be
// assigned to {})
Expand Down

0 comments on commit 9295054

Please sign in to comment.