| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- const Schmervice = require('@hapipal/schmervice')
- const haversine = require('haversine')
- const config = require('../../../db/data-generator/config.json')
- const profiler = require('./profiler')
- const scoring = require('./scorer')
- const zipcoder = require('./zipcoder')
-
- module.exports = class ProfileService extends Schmervice.Service {
- constructor(...args) {
- super(...args)
- /** Scores available in the db to map against score indices*/
- this.scoreLookup = {}
- /** Tags available in the db to map against tag_associations*/
- this.tagLookup = {}
- // this.responseKeyLookup = ResponseKey.query()
- }
- async _setScoreLookup() {
- if (!Object.keys(this.scoreLookup).length) {
- const { Aspect, AspectLabel } = this.server.models()
- const aspects = await Aspect.query()
- const labels = await AspectLabel.query()
- this.scoreLookup = scoring.makeScoreLookup(aspects, labels)
- }
- }
- async _setTagLookup() {
- /** Grab tag descriptions if they do NOT exist: Needed once per app load */
- if (Object.keys(this.tagLookup).length) return
- const { Tag } = this.server.models()
- const allTagDescriptions = await Tag.query()
- allTagDescriptions.forEach(desc => {
- if (!desc.is_active) return
- this.tagLookup[desc.tag_id] = desc
- })
- }
- /**
- * Internal method to get list of profile_ids for this user
- * @param {number} userId
- * @returns {Array} List of all profile_ids for user
- */
- async _getProfileIdsForUserId(userId) {
- const { Profile } = this.server.models()
-
- /** Grab every Profile associated with this id */
- const allProfiles = await Profile.query().where('user_id', userId)
-
- /** Copy a list of the just the Profiles */
- const profileIdsToGrab = allProfiles.map(profile => profile.profile_id)
-
- /** Uncomment to dedupe the list just in case */
- return [...new Set(profileIdsToGrab)]
- }
-
- async getProfile(profileId) {
- const { Profile } = this.server.models()
- await this._setTagLookup()
- const matchingProfile = await Profile.query()
- .where('profile_id', profileId)
- .first()
- .withGraphFetched('tags')
- .withGraphFetched('responses')
- .withGraphFetched('user')
- matchingProfile.tags = matchingProfile.tags.map(
- tag => this.tagLookup[tag.tag_id],
- )
- const complete = new profiler.CompleteProfile(matchingProfile, true)
- // TODO: Refactor to use this.setDefaults(), currently does not play
- // nice with reveal functionality
- return complete
- // return this.setDefaults(complete)
- }
-
- async getCompleteProfilesFor(userId, type) {
- const { Profile } = this.server.models()
- await this._setTagLookup()
-
- const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
-
- const profilesEntries = await Profile.query()
- .whereIn('profile_id', dedupedProfileIds)
- .withGraphFetched('tags')
- .withGraphFetched('responses')
- // CHECKTHIS: Added this because we added user.user_name to CompleteProfile
- // so without this, we get undefined user_name
- .withGraphFetched('user')
-
- return profiler.makeCompleteFromProfileEntries(
- profilesEntries,
- type,
- this.tagLookup,
- )
- }
-
- async getProfilesFor(profileIdArray, type, includeResponses = true) {
- const { Profile } = this.server.models()
- await this._setScoreLookup()
- await this._setTagLookup()
-
- // profilesEntries is profiles in dataaspect_labelsbase row order
- const profilesEntries = await Profile.query()
- .whereIn('profile_id', profileIdArray)
- .withGraphFetched('tags')
- .withGraphFetched('responses')
- .withGraphFetched('user')
-
- // taking the info from profilesEntries
- // to repack into completeProfiles
- // in same order as profileIdArray
- return profiler.makeOrderedCompleteProfiles(
- profileIdArray,
- profilesEntries,
- type,
- includeResponses,
- this.tagLookup,
- )
- }
-
- /**
- * Save responses in a profile
- * @param {number} userId
- * @param {Array} responses
- * @returns {object}
- */
- async saveResponsesCreateProfileFor(userId, responses, txn) {
- const { Profile, Response } = this.server.models()
-
- const profile = await Profile.query(txn).insert({
- user_id: userId,
- })
- for (const responseToSave of responses) {
- /**
- * Convert indexes to actual score values
- * Using using the input and converting to index
- * of the generated possible prescore array in config
- * DUPLICATE:See saveResponseForProfile() line 343
- */
- let convertedResponse = responseToSave
- if (scoring._isScorableResponse(responseToSave.response_key_id)) {
- // Convert -3 to 0, 0 to 3, 3 to 6
- const offset = (config.scoreVals.length - 1) / 2
- const indexFromInput = parseInt(responseToSave.val) + offset
- convertedResponse.val =
- config.scoreVals[indexFromInput].toString()
- }
-
- const responseInfo = {
- profile_id: profile.id,
- response_key_id: convertedResponse.response_key_id,
- val: convertedResponse.val,
- }
- await Response.query(txn).insert(responseInfo)
- }
- //** Work around for HAPI returning profile_id as id */
- return { user_id: profile.user_id, profile_id: profile.id }
- }
-
- /** Update responses in place
- * @param {number} profileId
- * @param {Array} responses
- * @returns {Array} updated responses
- */
- async updateResponsesInProfile(profileId, responses, txn) {
- const { Response } = this.server.models()
- for (const responseToSave of responses) {
- await Response.query(txn)
- .update({
- response_id: responseToSave.response_id,
- profile_id: responseToSave.profile_id,
- response_key_id: responseToSave.response_key_id,
- val: responseToSave.val,
- })
- .where({
- profile_id: profileId,
- })
- .where({
- response_id: responseToSave.response_id,
- })
- }
- return await Response.query(txn).where({
- profile_id: profileId,
- })
- }
-
- /** Add response
- * @param {Object} response to save
- * @returns {null} updated responses
- * @returns {Array} updated responses
- */
- async saveResponseForProfile(profileId, responseToSave) {
- const { Response } = this.server.models()
- let allResponses = await Response.query().where({
- profile_id: profileId,
- })
-
- // Delete matches
- // ?:Maybe bad idea
- const matchingResponses = allResponses.filter(
- response =>
- response.response_key_id == responseToSave.response_key_id,
- )
- if (matchingResponses.length > 0) {
- const alreadyAnswered = matchingResponses.map(
- matchingRes => matchingRes.response_key_id,
- )
- await Response.query()
- .where({ profile_id: profileId })
- .delete()
- .whereIn('response_key_id', alreadyAnswered)
- }
-
- /**
- * Convert indexes to actual score values
- * Using using the input and converting to index
- * of the generated possible prescore array in config
- */
- let convertedResponse = responseToSave
- if (scoring._isScorableResponse(responseToSave.response_key_id)) {
- // Convert -3 to 0, 0 to 3, 3 to 6
- const offset = (config.scoreVals.length - 1) / 2
- const scoreFromInput = parseInt(responseToSave.val) + offset
- const scoreFromConfig = config.scoreVals.indexOf(scoreFromInput)
- if (scoreFromConfig < 0) {
- console.error('score not found in possible config responses')
- }
- convertedResponse.val = scoreFromConfig.toString()
- }
-
- await Response.query().insert(convertedResponse)
- return allResponses
- }
-
- /**
- * Sets default profile attributes
- * @param {object} complete
- * @returns {object} updated profile
- */
- setDefaults(complete) {
- let defaultValues = {
- user_email: 'hidden@email.com',
- user_name: 'Hidden Name',
- }
-
- let defaultProfile = {
- ...complete,
- user_email: defaultValues.user_email,
- user_name: defaultValues.user_name,
- }
- console.log('---')
- console.log('defaultProfile: ', defaultProfile.user_email)
- if (!complete.reveal.length) return defaultProfile // nothing to reveal
-
- // swap out values of keys that are not found as complete.reveal.tag.tag_description values
- for (let [attribute, defaultVal] of Object.entries(defaultValues)) {
- if (
- typeof complete.reveal.find(
- tag => tag.tag_description == attribute,
- ) == 'undefined'
- )
- complete[attribute] = defaultVal
- }
- console.log('complete: ', complete.user_email)
-
- return complete
- }
-
- /**
- * Delete a profile
- * @param {number} userId
- * @param {number} profileId
- * @returns
- */
- async deleteProfile(userId, profileId) {
- const { Profile } = this.server.models()
-
- const dedupedGroupings = await this._getProfileIdsForUserId(userId)
-
- /** Do NOTHING if NOT in Grouping */
- if (!dedupedGroupings.includes(profileId)) return
-
- return await Profile.query().delete().where('profile_id', profileId)
- }
-
- /**
- * Score a profile
- * @param {number} profileId
- * @returns {Array} Ordered and scored Profiles
- */
- async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
- const { Profile } = this.server.models()
-
- await this._setScoreLookup()
-
- // Our User Profile to score for
- const userProfile = await Profile.query()
- .findOne('profile_id', profileId)
- .withGraphFetched('responses')
- .withGraphFetched('user')
-
- // Move unneeded responses
- const userZip = zipcoder.getZipCodeFromProfile(userProfile)
-
- // Find all Profiles that are NOT of our userProfile.type
- // ie. If userProfile.type == seeker, then find: poster
- let profileIdsOfOppositeType = await Profile.query()
- .withGraphFetched('responses')
- .withGraphFetched('user')
- // TODO: Let Objection optimize this
- const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
- profileIdsOfOppositeType = profileIdsOfOppositeType
- .filter(profile => {
- return profile.user.is_poster == isPosterOpposite
- })
- .filter(profile => {
- // Only include profiles that included zipcode response
- return zipcoder.getZipCodeFromProfile(profile) ? true : false
- })
-
- const profilePlusDistance = await Promise.all(
- profileIdsOfOppositeType.map(async profile => {
- const targetZip = zipcoder.getZipCodeFromProfile(profile)
-
- if (!userZip || !targetZip)
- return { ...profile, distance: [9999, distanceUnit] }
-
- const distance = await this._compareDistance(
- userZip,
- targetZip,
- distanceUnit,
- )
- return {
- ...profile,
- distance: [distance.toFixed(2), distanceUnit],
- }
- }),
- )
-
- const distanceFilteredProfiles = zipcoder.filterByDistance(
- profilePlusDistance,
- maxDistance,
- )
- const scoredProfilesWithDistance = scoring.scoreAll(
- distanceFilteredProfiles,
- userProfile,
- this.scoreLookup,
- )
- // Order by score
- return scoredProfilesWithDistance.sort(
- (a, b) => b.score.total - a.score.total,
- )
- }
-
- /**
- * Use the db for zipcode info
- * @param {string} zipCode
- * @param {object}
- */
- async _latLonForZip(zipCode) {
- const { ZipCode } = this.server.models()
- const zipInfo = await ZipCode.query().findOne(
- 'zip_code_id',
- parseInt(zipCode),
- )
- if (!zipInfo) {
- console.error('zip:', zipCode)
- }
- return {
- latitude: parseFloat(zipInfo.latitude),
- longitude: parseFloat(zipInfo.longitude),
- }
- }
- /**
- * Get the distance between two zipcodes
- * using the haversine formula
- * @param {string} start_zip
- * @param {string} end_zip
- * @param {number} distance in miles
- */
- async _compareDistance(start_zip, end_zip, distanceUnit) {
- if (!start_zip || !end_zip || isNaN(start_zip) || isNaN(end_zip)) return
- const start = await this._latLonForZip(start_zip)
- const end = await this._latLonForZip(end_zip)
- return haversine(start, end, { unit: distanceUnit })
- }
-
- /**
- * Use the db to grab tag associations
- * by profile and match them to tag types
- * @param {number} profileId
- * @param {object}
- */
- async getTagsFor(profileId, groupingId, category) {
- const { TagAssociation } = this.server.models()
- await this._setTagLookup()
- let associations = groupingId
- ? await TagAssociation.query()
- .where('grouping_id', groupingId)
- .andWhere('profile_id', profileId)
- : await TagAssociation.query().andWhere('profile_id', profileId)
- return associations
- .map(assoc => ({
- ...assoc,
- tag: this.tagLookup[assoc.tag_id],
- }))
- .filter(tagWithAssoc => {
- return category
- ? tagWithAssoc.tag.tag_category == category
- : true
- })
- }
-
- /**
- * Use the db to grab tag associations
- * by profile, grouping, tag, and insert
- * it if it already exists
- * @param {object} association
- */
- async revealProfileInfo(association) {
- const { TagAssociation } = this.server.models()
-
- const existingAssociations = await TagAssociation.query()
- .where('profile_id', `${association.profile_id}`)
- .where('grouping_id', `${association.grouping_id}`)
- .where('tag_id', `${association.tag_id}`)
- .where('is_deleted', 0)
- if (!existingAssociations.length) {
- await TagAssociation.query().insert(association)
- return await this.getTagsFor(association.profile_id)
- } else {
- return console.error('tag association already exists')
- }
- }
- }
|