Skip to content

Commit

Permalink
Merge pull request #313 from OpenBeta/kao-ticksbyusername
Browse files Browse the repository at this point in the history
Query ticks by username
  • Loading branch information
zichongkao authored Jun 13, 2023
2 parents 050ee0f + 7b69d09 commit 9e3318e
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 33 deletions.
67 changes: 64 additions & 3 deletions src/__tests__/ticks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { jest } from '@jest/globals'
import { queryAPI, setUpServer } from '../utils/testUtils.js'
import { muuidToString } from '../utils/helpers.js'
import { TickInput } from '../db/TickTypes.js'
import TickDataSource from '../model/TickDataSource.js'
import UserDataSource from '../model/UserDataSource.js'
import { UpdateProfileGQLInput } from '../db/UserTypes.js'

jest.setTimeout(60000)

Expand All @@ -14,13 +17,13 @@ describe('ticks API', () => {
let inMemoryDB

// Mongoose models for mocking pre-existing state.
let ticks: TickDataSource
let users: UserDataSource
let tickOne: TickInput

beforeAll(async () => {
({ server, inMemoryDB } = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
user = muuid.v4()
userUuid = muuidToString(user)

tickOne = {
Expand All @@ -37,6 +40,8 @@ describe('ticks API', () => {
})

beforeEach(async () => {
ticks = TickDataSource.getInstance()
users = UserDataSource.getInstance()
await inMemoryDB.clear()
})

Expand All @@ -45,6 +50,62 @@ describe('ticks API', () => {
await inMemoryDB.close()
})

describe('queries', () => {
const userQuery = `
query userTicks($userId: MUUID, $username: String) {
userTicks(userId: $userId, username: $username) {
_id
name
notes
climbId
style
attemptType
dateClimbed
grade
userId
}
}
`

it('queries by userId', async () => {
const userProfileInput: UpdateProfileGQLInput = {
userUuid,
username: 'cat.dog',
email: '[email protected]'
}
await users.createOrUpdateUserProfile(user, userProfileInput)
await ticks.addTick(tickOne)
const response = await queryAPI({
query: userQuery,
variables: { userId: userUuid },
userUuid
})
expect(response.statusCode).toBe(200)
const res = response.body.data.userTicks
expect(res).toHaveLength(1)
expect(res[0].name).toBe(tickOne.name)
})

it('queries by username', async () => {
const userProfileInput: UpdateProfileGQLInput = {
userUuid,
username: 'cat.dog',
email: '[email protected]'
}
await users.createOrUpdateUserProfile(user, userProfileInput)
await ticks.addTick(tickOne)
const response = await queryAPI({
query: userQuery,
variables: { username: 'cat.dog' },
userUuid
})
expect(response.statusCode).toBe(200)
const res = response.body.data.userTicks
expect(res).toHaveLength(1)
expect(res[0].name).toBe(tickOne.name)
})
})

describe('mutations', () => {
const createQuery = `
mutation ($input: Tick!) {
Expand Down
2 changes: 1 addition & 1 deletion src/auth/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const createContext = async ({ req }): Promise<any> => {
payload = await verifyJWT(token)
} catch (e) {
logger.error(`Can't verify JWT token ${e.toString() as string}`)
throw new Error('An unxpected error has occurred. Please notify us at [email protected].')
throw new Error('An unexpected error has occurred. Please notify us at [email protected].')
}

user.isBuilder = payload?.scope?.includes('builder:default') ?? false
Expand Down
6 changes: 6 additions & 0 deletions src/db/TickTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import mongoose from 'mongoose'
import { MUUID } from 'uuid-mongodb'

/**
* Ticks may be sourced from a number of places. They may come from external sources,
Expand Down Expand Up @@ -101,3 +102,8 @@ export interface TickInput {
export interface TickEditFilterType {
_id: mongoose.Types.ObjectId
}

export interface TickUserSelectors {
userId?: MUUID
username?: string
}
4 changes: 2 additions & 2 deletions src/db/utils/jobs/migration/CreateUsersCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { User as Auth0User } from 'auth0'
import { connectDB, gracefulExit, getUserModel } from '../../../index.js'
import { logger } from '../../../../logger.js'
import { User, UpdateProfileGQLInput } from '../../../UserTypes.js'
import { nonAlphanumericRegex } from '../../../../model/UserDataSource.js'
import { canonicalizeUsername } from '../../../../utils/helpers.js'

const LOCAL_MEDIA_DIR_UID = process.env.LOCAL_MEDIA_DIR_UID

Expand Down Expand Up @@ -56,7 +56,7 @@ const onConnected = async (): Promise<void> => {
avatar,
usernameInfo: {
username,
canonicalName: username.replaceAll(nonAlphanumericRegex, ''),
canonicalName: canonicalizeUsername(username),
updatedAt: new Date(ts)
},
createdBy: userUuid
Expand Down
4 changes: 2 additions & 2 deletions src/graphql/schema/Tick.gql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
type Query {
"""
Gets all of the users current ticks by their Auth-0 userId
Gets all of the users current ticks by their Auth-0 userId or username
"""
userTicks(userId: String): [TickType]
userTicks(userId: MUUID, username: String): [TickType]
"""
Gets all of the users current ticks for a specific climb by their
Auth-0 userId and Open-Beta ClimbId
Expand Down
9 changes: 4 additions & 5 deletions src/graphql/tick/TickQueries.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { TickType } from '../../db/TickTypes'
import { TickType, TickUserSelectors } from '../../db/TickTypes'
import type TickDataSource from '../../model/TickDataSource'

const TickQueries = {
userTicks: async (_, input, { dataSources }): Promise<TickType[] | null> => {
userTicks: async (_, input: TickUserSelectors, { dataSources }): Promise<TickType[] | null> => {
const { ticks }: { ticks: TickDataSource } = dataSources
const { userId } = input
return await ticks.ticksByUser(userId)
return await ticks.ticksByUser(input)
},
userTicksByClimbId: async (_, input, { dataSources }): Promise<TickType[] | null> => {
const { ticks }: { ticks: TickDataSource } = dataSources
const { climbId, userId } = input
return await ticks.ticksByUserAndClimb(userId, climbId)
return await ticks.ticksByUserIdAndClimb(userId, climbId)
}
}

Expand Down
40 changes: 35 additions & 5 deletions src/model/TickDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { MongoDataSource } from 'apollo-datasource-mongodb'
import type { DeleteResult } from 'mongodb'
import mongoose from 'mongoose'

import { TickEditFilterType, TickInput, TickType } from '../db/TickTypes'
import { getTickModel } from '../db/index.js'
import { TickEditFilterType, TickInput, TickType, TickUserSelectors } from '../db/TickTypes'
import { getTickModel, getUserModel } from '../db/index.js'
import type { User } from '../db/UserTypes'

export default class TickDataSource extends MongoDataSource<TickType> {
tickModel = getTickModel()
userModel = getUserModel()

/**
* @param tick takes in a new tick
Expand Down Expand Up @@ -71,11 +73,39 @@ export default class TickDataSource extends MongoDataSource<TickType> {
}
}

async ticksByUser (userId: string): Promise<TickType[]> {
return await this.tickModel.find({ userId })
/**
* Retrieve ticks of a user given their details
* @param userSelectors Attributes that can be used to identify the user
* @returns
*/
async ticksByUser (userSelectors: TickUserSelectors): Promise<TickType[]> {
const { userId: requestedUserId, username } = userSelectors
if (requestedUserId == null && username == null) {
throw new Error('Username or userId must be supplied')
}
const filters: any[] = []
if (requestedUserId != null) {
filters.push({ _id: requestedUserId })
}
if (username != null) {
filters.push({
'usernameInfo.username': {
$exists: true, $eq: username
}
})
}
const userIdObject = await this.userModel.findOne<Pick<User, '_id'>>(
{ $or: filters },
{ _id: 1 }
).lean()
if (userIdObject == null) {
throw new Error('No such user')
}
// Unfortunately, userIds on ticks are stored as strings not MUUIDs.
return await this.tickModel.find({ userId: userIdObject._id.toUUID().toString() })
}

async ticksByUserAndClimb (userId: string, climbId: string): Promise<TickType[]> {
async ticksByUserIdAndClimb (userId: string, climbId: string): Promise<TickType[]> {
return await this.tickModel.find({ userId, climbId })
}

Expand Down
9 changes: 4 additions & 5 deletions src/model/UserDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {
UserPublicProfile
} from '../db/UserTypes.js'
import { trimToNull } from '../utils/sanitize.js'
import { canonicalizeUsername } from '../utils/helpers.js'

const USERNAME_UPDATE_WAITING_IN_DAYS = 14

export const nonAlphanumericRegex = /[\W_\s]+/g

export default class UserDataSource extends MongoDataSource<User> {
static PUBLIC_PROFILE_PROJECTION = {
_id: 1,
Expand Down Expand Up @@ -44,7 +43,7 @@ export default class UserDataSource extends MongoDataSource<User> {
const rs = await this.userModel.find(
{
'usernameInfo.canonicalName': {
$exists: true, $eq: _username.replaceAll(nonAlphanumericRegex, '')
$exists: true, $eq: canonicalizeUsername(_username)
}
},
{
Expand Down Expand Up @@ -119,7 +118,7 @@ export default class UserDataSource extends MongoDataSource<User> {
if (username != null) {
usernameInfo = {
username,
canonicalName: username.replaceAll(nonAlphanumericRegex, ''),
canonicalName: canonicalizeUsername(username),
updatedAt: new Date()
}
}
Expand Down Expand Up @@ -155,7 +154,7 @@ export default class UserDataSource extends MongoDataSource<User> {
}

rs.set('usernameInfo.username', username)
rs.set('usernameInfo.canonicalName', username.replaceAll(nonAlphanumericRegex, ''))
rs.set('usernameInfo.canonicalName', canonicalizeUsername(username))
}

if (displayName != null && displayName !== rs.displayName) {
Expand Down
33 changes: 23 additions & 10 deletions src/model/__tests__/ticks.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import mongoose from 'mongoose'
import { produce } from 'immer'
import TickDataSource from '../TickDataSource.js'
import { connectDB, getTickModel } from '../../db/index.js'
import { connectDB, getTickModel, getUserModel } from '../../db/index.js'
import { TickInput } from '../../db/TickTypes.js'
import muuid from 'uuid-mongodb'
import UserDataSource from '../UserDataSource.js'
import { UpdateProfileGQLInput } from '../../db/UserTypes.js'

const userId = muuid.v4()

const toTest: TickInput = {
name: 'Small Dog',
notes: 'Sandbagged',
climbId: 'c76d2083-6b8f-524a-8fb8-76e1dc79833f',
userId: 'abc123',
userId: userId.toUUID().toString(),
style: 'Lead',
attemptType: 'Onsight',
dateClimbed: new Date('2012-12-12'),
Expand All @@ -20,7 +25,7 @@ const toTest2: TickInput = {
name: 'Sloppy Peaches',
notes: 'v sloppy',
climbId: 'b767d949-0daf-5af3-b1f1-626de8c84b2a',
userId: 'abc123',
userId: userId.toUUID().toString(),
style: 'Lead',
attemptType: 'Flash',
dateClimbed: new Date('2012-10-15'),
Expand All @@ -42,17 +47,21 @@ describe('Ticks', () => {
let ticks: TickDataSource
const tickModel = getTickModel()

let users: UserDataSource

beforeAll(async () => {
console.log('#BeforeAll Ticks')
await connectDB()

try {
await getTickModel().collection.drop()
await getUserModel().collection.drop()
} catch (e) {
console.log('Cleaning db')
}

ticks = new TickDataSource(mongoose.connection.db.collection('ticks'))
ticks = TickDataSource.getInstance()
users = UserDataSource.getInstance()
})

afterAll(async () => {
Expand Down Expand Up @@ -122,13 +131,19 @@ describe('Ticks', () => {
})

it('should grab all ticks by userId', async () => {
const userProfileInput: UpdateProfileGQLInput = {
userUuid: userId.toUUID().toString(),
username: 'cat.dog',
email: '[email protected]'
}
await users.createOrUpdateUserProfile(userId, userProfileInput)
const tick = await ticks.addTick(toTest)

if (tick == null) {
fail('Should add a new tick')
}

const newTicks = await ticks.ticksByUser('abc123')
const newTicks = await ticks.ticksByUser({ userId })

expect(newTicks.length).toEqual(1)
})
Expand All @@ -141,33 +156,31 @@ describe('Ticks', () => {
if (tick == null || tick2 == null) {
fail('Should add a new tick')
}
const userClimbTicks = await ticks.ticksByUserAndClimb('abc123', climbId)
const userClimbTicks = await ticks.ticksByUserIdAndClimb(userId.toUUID().toString(), climbId)
expect(userClimbTicks.length).toEqual(1)
})

it('should delete all ticks with the specified userId', async () => {
const userId = 'abc123'
const newTicks = await ticks.importTicks(testImport)

if (newTicks == null) {
fail('Should add 3 new ticks')
}

await ticks.deleteAllTicks(userId)
await ticks.deleteAllTicks(userId.toUUID().toString())
const newTick = await tickModel.findOne({ userId })
expect(newTick).toBeNull()
})

it('should only delete MP imports', async () => {
const userId = 'abc123'
const MPTick = await ticks.addTick(toTest)
const OBTick = await ticks.addTick(tickUpdate)

if (MPTick == null || OBTick == null) {
fail('Should add two new ticks')
}

await ticks.deleteImportedTicks(userId)
await ticks.deleteImportedTicks(userId.toUUID().toString())
const newTick = await tickModel.findOne({ _id: OBTick._id })
expect(newTick?._id).toEqual(OBTick._id)
expect(newTick?.notes).toEqual('Not sandbagged')
Expand Down
3 changes: 3 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ export function exhaustiveCheck (_value: never): never {

export const geojsonPointToLongitude = (point: Point): number => point.coordinates[0]
export const geojsonPointToLatitude = (point: Point): number => point.coordinates[1]

export const NON_ALPHANUMERIC_REGEX = /[\W_\s]+/g
export const canonicalizeUsername = (username: string): string => username.replaceAll(NON_ALPHANUMERIC_REGEX, '')

0 comments on commit 9e3318e

Please sign in to comment.