UNPKG

@wepublish/api-db-mongodb

Version:

We.publish Database adapter for mongoDB

481 lines (420 loc) 13.1 kB
import bcrypt from 'bcrypt' import { ConnectionResult, CreateUserArgs, DBUserAdapter, DeleteUserArgs, DeleteUserOAuth2AccountArgs, GetUserByOAuth2AccountArgs, GetUserForCredentialsArgs, GetUsersArgs, InputCursorType, LimitType, OptionalUser, ResetUserPasswordArgs, SortOrder, UpdatePaymentProviderCustomerArgs, UpdateUserArgs, User, UserOAuth2Account, UserOAuth2AccountArgs, UserSort } from '@wepublish/api' import {Collection, Db, FilterQuery, MongoCountPreferences, MongoError} from 'mongodb' import {CollectionName, DBUser} from './schema' import {escapeRegExp, MongoErrorCode} from '../utility' import {MaxResultsPerPage} from './defaults' import {Cursor} from './cursor' export class MongoDBUserAdapter implements DBUserAdapter { private users: Collection<DBUser> private bcryptHashCostFactor: number private locale: string constructor(db: Db, bcryptHashCostFactor: number, locale: string) { this.users = db.collection(CollectionName.Users) this.bcryptHashCostFactor = bcryptHashCostFactor this.locale = locale } async createUser({input, password}: CreateUserArgs): Promise<OptionalUser> { try { const passwordHash = await bcrypt.hash(password, this.bcryptHashCostFactor) const {insertedId: id} = await this.users.insertOne({ createdAt: new Date(), modifiedAt: new Date(), email: input.email, emailVerifiedAt: null, oauth2Accounts: [], name: input.name, firstName: input.firstName, preferredName: input.preferredName, address: input.address, active: input.active, lastLogin: null, properties: input.properties, roleIDs: input.roleIDs, password: passwordHash, paymentProviderCustomers: input.paymentProviderCustomers || [] }) return this.getUserByID(id) } catch (err) { if (err instanceof MongoError && err.code === MongoErrorCode.DuplicateKey) { throw new Error('Email address already exists!') } throw err } } async getUser(email: string): Promise<OptionalUser> { const user = await this.users.findOne({email}) if (user) { return { id: user._id, createdAt: user.createdAt, modifiedAt: user.modifiedAt, email: user.email, emailVerifiedAt: user.emailVerifiedAt, oauth2Accounts: user.oauth2Accounts, name: user.name, firstName: user.firstName, preferredName: user.preferredName, address: user.address, active: user.active, lastLogin: user.lastLogin, properties: user.properties, roleIDs: user.roleIDs, paymentProviderCustomers: user.paymentProviderCustomers } } else { return null } } async updateUser({id, input}: UpdateUserArgs): Promise<OptionalUser> { const {value} = await this.users.findOneAndUpdate( {_id: id}, { $set: { modifiedAt: new Date(), name: input.name, firstName: input.firstName, preferredName: input.preferredName, address: input.address, active: input.active, properties: input.properties, email: input.email, emailVerifiedAt: input.emailVerifiedAt, roleIDs: input.roleIDs } }, {returnOriginal: false} ) if (!value) return null const {_id: outID} = value return this.getUserByID(outID) } async deleteUser({id}: DeleteUserArgs): Promise<string | null> { const {deletedCount} = await this.users.deleteOne({_id: id}) return deletedCount !== 0 ? id : null } async resetUserPassword({id, password}: ResetUserPasswordArgs): Promise<OptionalUser> { const {value} = await this.users.findOneAndUpdate( {_id: id}, { $set: { modifiedAt: new Date(), password: await bcrypt.hash(password, this.bcryptHashCostFactor) } }, {returnOriginal: false} ) if (!value) return null const {_id: outID} = value return this.getUserByID(outID) } async getUsersByID(ids: string[]): Promise<OptionalUser[]> { const users = await this.users.find({_id: {$in: ids}}).toArray() return users.map(user => { return { id: user._id, createdAt: user.createdAt, modifiedAt: user.modifiedAt, email: user.email, emailVerifiedAt: user.emailVerifiedAt, oauth2Accounts: user.oauth2Accounts, name: user.name, firstName: user.firstName, preferredName: user.preferredName, address: user.address, active: user.active, lastLogin: user.lastLogin, properties: user.properties, roleIDs: user.roleIDs, paymentProviderCustomers: user.paymentProviderCustomers } }) } async getUserForCredentials({email, password}: GetUserForCredentialsArgs): Promise<OptionalUser> { const user = await this.users.findOne({email}) if (user && (await bcrypt.compare(password, user.password))) { return { id: user._id, createdAt: user.createdAt, modifiedAt: user.modifiedAt, email: user.email, emailVerifiedAt: user.emailVerifiedAt, oauth2Accounts: user.oauth2Accounts, name: user.name, firstName: user.firstName, preferredName: user.preferredName, address: user.address, active: user.active, lastLogin: user.lastLogin, properties: user.properties, roleIDs: user.roleIDs, paymentProviderCustomers: user.paymentProviderCustomers } } return null } async getUserByID(id: string): Promise<OptionalUser> { const user = await this.users.findOne({_id: id}) if (user) { return { id: user._id, createdAt: user.createdAt, modifiedAt: user.modifiedAt, email: user.email, emailVerifiedAt: user.emailVerifiedAt, oauth2Accounts: user.oauth2Accounts, name: user.name, firstName: user.firstName, preferredName: user.preferredName, address: user.address, active: user.active, lastLogin: user.lastLogin, properties: user.properties, roleIDs: user.roleIDs, paymentProviderCustomers: user.paymentProviderCustomers } } else { return null } } async getUserByOAuth2Account({ provider, providerAccountId }: GetUserByOAuth2AccountArgs): Promise<OptionalUser> { const user = await this.users.findOne({ oauth2Accounts: {$elemMatch: {provider, providerAccountId}} }) if (user) { return { id: user._id, createdAt: user.createdAt, modifiedAt: user.modifiedAt, email: user.email, emailVerifiedAt: user.emailVerifiedAt, oauth2Accounts: user.oauth2Accounts, name: user.name, firstName: user.firstName, preferredName: user.preferredName, address: user.address, active: user.active, lastLogin: user.lastLogin, properties: user.properties, roleIDs: user.roleIDs, paymentProviderCustomers: user.paymentProviderCustomers } } else { return null } } async getUsers({ filter, sort, order, cursor, limit }: GetUsersArgs): Promise<ConnectionResult<User>> { const limitCount = Math.min(limit.count, MaxResultsPerPage) const sortDirection = limit.type === LimitType.First ? order : -order const cursorData = cursor.type !== InputCursorType.None ? Cursor.from(cursor.data) : undefined const expr = order === SortOrder.Ascending ? cursor.type === InputCursorType.After ? '$gt' : '$lt' : cursor.type === InputCursorType.After ? '$lt' : '$gt' const sortField = userSortFieldForSort(sort) const cursorFilter = cursorData ? { $or: [ {[sortField]: {[expr]: cursorData.date}}, {_id: {[expr]: cursorData.id}, [sortField]: cursorData.date} ] } : {} const textFilter: FilterQuery<any> = {} if (filter && JSON.stringify(filter) !== '{}') { textFilter.$and = [] } // TODO: Rename to search if (filter?.name !== undefined) { textFilter.$and?.push({name: {$regex: escapeRegExp(filter.name), $options: 'i'}}) } if (filter?.text !== undefined) { const columnsToSearch = [ 'name', 'firstName', 'email', 'address.streetAddress', 'address.zipCode', 'address.city' ] const orConditions = [] const search = filter?.text const searchTerms = search.split(' ') // iterate user search terms for (const searchTerm of searchTerms) { // iterate columns to be searched for (const column of columnsToSearch) { const orCondition: any = {} orCondition[column] = { $regex: escapeRegExp(searchTerm), $options: 'im' } orConditions.push(orCondition) } } textFilter.$and?.push({ $or: orConditions }) } const [totalCount, users] = await Promise.all([ this.users.countDocuments(textFilter, { collation: {locale: this.locale, strength: 2} } as MongoCountPreferences), // MongoCountPreferences doesn't include collation this.users .aggregate([], {collation: {locale: this.locale, strength: 2}}) .match(textFilter) .match(cursorFilter) .sort({[sortField]: sortDirection, _id: sortDirection}) .skip(limit.skip ?? 0) .limit(limitCount + 1) .toArray() ]) const nodes = users.slice(0, limitCount) if (limit.type === LimitType.Last) { nodes.reverse() } const hasNextPage = limit.type === LimitType.First ? users.length > limitCount : cursor.type === InputCursorType.Before const hasPreviousPage = limit.type === LimitType.Last ? users.length > limitCount : cursor.type === InputCursorType.After const firstUser = nodes[0] const lastUser = nodes[nodes.length - 1] const startCursor = firstUser ? new Cursor(firstUser._id, userDateForSort(firstUser, sort)).toString() : null const endCursor = lastUser ? new Cursor(lastUser._id, userDateForSort(lastUser, sort)).toString() : null return { nodes: nodes.map<User>(({_id: id, ...user}) => ({id, ...user})), pageInfo: { startCursor, endCursor, hasNextPage, hasPreviousPage }, totalCount } } async updatePaymentProviderCustomers({ userID, paymentProviderCustomers }: UpdatePaymentProviderCustomerArgs): Promise<OptionalUser> { const {value} = await this.users.findOneAndUpdate( {_id: userID}, { $set: { modifiedAt: new Date(), paymentProviderCustomers: paymentProviderCustomers } }, {returnOriginal: false} ) if (!value) return null const {_id: outID} = value return this.getUserByID(outID) } async addOAuth2Account({userID, oauth2Account}: UserOAuth2AccountArgs): Promise<OptionalUser> { const user = await this.users.findOne({_id: userID}) if (!user) return null const accounts: UserOAuth2Account[] = [...user.oauth2Accounts, oauth2Account] const {value} = await this.users.findOneAndUpdate( {_id: userID}, { $set: { modifiedAt: new Date(), oauth2Accounts: accounts } }, {returnOriginal: false} ) if (!value) return null const {_id: outID} = value return this.getUserByID(outID) } async deleteOAuth2Account({ userID, providerAccountId, provider }: DeleteUserOAuth2AccountArgs): Promise<OptionalUser> { const user = await this.users.findOne({_id: userID}) if (!user) return null const {value} = await this.users.findOneAndUpdate( {_id: userID}, { $set: { modifiedAt: new Date(), oauth2Accounts: user.oauth2Accounts.filter( account => account.provider !== provider && account.providerAccountId !== providerAccountId ) } }, {returnOriginal: false} ) if (!value) return null const {_id: outID} = value return this.getUserByID(outID) } } function userSortFieldForSort(sort: UserSort) { switch (sort) { case UserSort.CreatedAt: return 'createdAt' case UserSort.ModifiedAt: return 'modifiedAt' case UserSort.Name: return 'name' case UserSort.FirstName: return 'firstName' } } function userDateForSort(user: DBUser, sort: UserSort): Date { switch (sort) { case UserSort.CreatedAt: return user.createdAt case UserSort.ModifiedAt: return user.modifiedAt case UserSort.Name: return user.createdAt case UserSort.FirstName: return user.createdAt } }