diff --git a/convertors/set-user-project-last-visit.js b/convertors/set-user-project-last-visit.js new file mode 100644 index 00000000..fb5b31ef --- /dev/null +++ b/convertors/set-user-project-last-visit.js @@ -0,0 +1,53 @@ +require('dotenv').config(); +require('process'); +const { setup } = require('./setup'); + +/** + * Method that runs convertor script + */ +async function run() { + const { client, hawkDb } = await setup(); + + const collections = await hawkDb.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + let usersInProjectCollectionsToCheck = collections.filter(col => /^users-in-project:/.test(col.name)).map(col => col.name); + + console.log(`Found ${usersInProjectCollectionsToCheck.length} users in project collections.`); + + const usersDocuments = await hawkDb.collection('users').find({}).toArray(); + + // Convert events + let i = 1; + + for (const collectionName of usersInProjectCollectionsToCheck) { + console.log(`[${i}/${usersInProjectCollectionsToCheck.length}] Processing ${collectionName}`); + + const usersInProject = await hawkDb.collection(collectionName).find({}).toArray(); + + console.log(`Found ${usersInProject.length} users in project ${collectionName}.`); + + let usersUpdatedCount = 0; + + for (const userInProject of usersInProject) { + const userDocument = usersDocuments.find(u => u._id.toString() === userInProject.userId.toString()); + if (userDocument) { + const projectId = collectionName.split(':')[1]; + await hawkDb.collection('users').updateOne({ _id: userDocument._id }, { $set: { projectsLastVisit: { [projectId]: userInProject.timestamp } } }); + usersUpdatedCount++; + console.log(`Updated ${usersUpdatedCount}/${usersInProject.length} users in project ${collectionName}.`); + } + } + + i++; + } + + await client.close(); +} + +run().catch(err => { + console.error('❌ Script failed:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/convertors/setup.js b/convertors/setup.js new file mode 100644 index 00000000..60bef2e8 --- /dev/null +++ b/convertors/setup.js @@ -0,0 +1,44 @@ +const { MongoClient } = require('mongodb'); + +async function setup() { + const fullUri = process.env.MONGO_HAWK_DB_URL; + + // Parse the Mongo URL manually + const mongoUrl = new URL(fullUri); + const hawkDatabaseName = 'hawk'; + + // Extract query parameters + const queryParams = Object.fromEntries(mongoUrl.searchParams.entries()); + + // Compose connection options manually + const options = { + useNewUrlParser: true, + useUnifiedTopology: true, + authSource: queryParams.authSource || 'admin', + replicaSet: queryParams.replicaSet || undefined, + tls: queryParams.tls === 'true', + tlsInsecure: queryParams.tlsInsecure === 'true', + // connectTimeoutMS: 3600000, + // socketTimeoutMS: 3600000, + }; + + // Remove query string from URI + mongoUrl.search = ''; + const cleanUri = mongoUrl.toString(); + + console.log('Connecting to:', cleanUri); + console.log('With options:', options); + + const client = new MongoClient(cleanUri, options); + + await client.connect(); + const hawkDb = client.db(hawkDatabaseName); + + console.log(`Connected to database: ${hawkDatabaseName}`); + + return { client, hawkDb }; +} + +module.exports = { setup }; + + diff --git a/package.json b/package.json index 01d3b220..7e032b73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.2.13", + "version": "1.2.14", "main": "index.ts", "license": "BUSL-1.1", "scripts": { @@ -11,6 +11,7 @@ "dev:up": "docker-compose -f docker-compose.dev.yml up -d", "dev:down": "docker-compose -f docker-compose.dev.yml down", "build": "tsc", + "convert": "node ./convertors/set-user-project-last-visit.js", "migrations:create": "docker-compose exec api yarn migrate-mongo create", "migrations:up": "docker-compose exec api yarn migrate-mongo up", "migrations:down": "docker-compose exec api yarn migrate-mongo down", diff --git a/src/models/user.ts b/src/models/user.ts index 7ea2f065..1913e3ce 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -62,7 +62,7 @@ export interface UserNotificationsDBScheme { /** * Types of notifications to receive */ - whatToReceive: {[key in UserNotificationType]: boolean}; + whatToReceive: { [key in UserNotificationType]: boolean }; } /** @@ -85,6 +85,11 @@ export enum UserNotificationType { SystemMessages = 'SystemMessages', } +/** + * This structure represents how user projects last visit is stored at the DB (in 'users' collection) + */ +type UserProjectsLastVisitDBScheme = Record; + /** * User model */ @@ -130,6 +135,11 @@ export default class UserModel extends AbstractModel implements Us */ public notifications!: UserNotificationsDBScheme; + /** + * User projects last visit + */ + public projectsLastVisit!: UserProjectsLastVisitDBScheme; + /** * Saved bank cards for one-click payments */ @@ -233,6 +243,33 @@ export default class UserModel extends AbstractModel implements Us } } + /** + * Update user's last project visit + * + * @param projectId - project id + * @returns {Promise} - last project visit timestamp + */ + public async updateLastProjectVisit(projectId: string): Promise { + const time = Date.now() / 1000; + + await this.update( + { _id: new ObjectId(this._id) }, + { [`projectsLastVisit.${projectId}`]: time } + ); + + return time; + } + + /** + * Get user's last project visit + * + * @param projectId - project id + * @returns {Promise} - last project visit timestamp + */ + public async getLastProjectVisit(projectId: string): Promise { + return this.projectsLastVisit?.[projectId] || 0; + } + /** * Update user profile data * @param user – user object @@ -323,7 +360,7 @@ export default class UserModel extends AbstractModel implements Us * Remove workspace from membership collection * @param workspaceId - id of workspace to remove */ - public async removeWorkspace(workspaceId: string): Promise<{workspaceId: string}> { + public async removeWorkspace(workspaceId: string): Promise<{ workspaceId: string }> { await this.membershipCollection.deleteOne({ workspaceId: new ObjectId(workspaceId), }); diff --git a/src/models/userInProject.js b/src/models/userInProject.js deleted file mode 100644 index 4cb89c73..00000000 --- a/src/models/userInProject.js +++ /dev/null @@ -1,89 +0,0 @@ -const mongo = require('../mongo'); -const { ObjectID } = require('mongodb'); - -/** - * User in project model - * This class works with project's settings - */ -class UserInProject { - /** - * @param {String} userId - user's identifier - * @param {ObjectId} projectId - project's identifier - */ - constructor(userId, projectId) { - this.userId = userId; - this.projectId = projectId; - } - - /** - * Model's collection - * @return {Collection} - */ - get collection() { - return mongo.databases.hawk.collection('users-in-project:' + this.projectId); - } - - /** - * Set's new timestamp when project is visited by user - * - * @return {Number} - */ - updateLastVisit() { - const time = Date.now() / 1000; - - this.collection.updateOne({ - userId: new ObjectID(this.userId), - }, { - $set: { - timestamp: time, - }, - }, { - upsert: true, - }); - - return time; - } - - /** - * Returns timestamp of last project visit - * - * @return {Promise} - */ - async getLastVisit() { - const result = await this.collection.findOne({ - userId: new ObjectID(this.userId), - }); - - return result && result.timestamp; - } - - /** - * Returns personal notifications settings for user - * @returns {Promise} - */ - async getPersonalNotificationsSettings() { - const result = await this.collection.findOne({ - userId: new ObjectID(this.userId), - }); - - return result && result.notificationSettings; - } - - /** - * Update Notify Settings - * - * @param {NotificationSettingsSchema} notificationSettings - settings to update - * @returns {Promise} - */ - async updatePersonalNotificationsSettings(notificationSettings) { - const updated = await this.collection.updateOne( - { userId: new ObjectID(this.userId) }, - { $set: { notificationSettings } }, - { upsert: true } - ); - - return updated.modifiedCount || updated.upsertedCount || updated.matchedCount; - } -} - -module.exports = UserInProject; diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 9513e497..5cf0bb68 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -3,7 +3,6 @@ import * as telegram from '../utils/telegram'; const mongo = require('../mongo'); const { ApolloError, UserInputError } = require('apollo-server-express'); const Validator = require('../utils/validator'); -const UserInProject = require('../models/userInProject'); const EventsFactory = require('../models/eventsFactory'); const getEventsFactory = require('./helpers/eventsFactory').default; const ProjectToWorkspace = require('../models/projectToWorkspace'); @@ -358,10 +357,14 @@ module.exports = { * @param {Context.user} user - current authorized user {@see ../index.js} * @return {Promise} */ - async updateLastProjectVisit(_obj, { projectId }, { user }) { - const userInProject = new UserInProject(user.id, projectId); + async updateLastProjectVisit(_obj, { projectId }, { user, factories }) { + const userModel = await factories.usersFactory.findById(user.id); - return userInProject.updateLastVisit(); + if (!userModel) { + throw new ApolloError('User not found'); + } + + return userModel.updateLastProjectVisit(projectId); }, }, Project: { @@ -422,10 +425,14 @@ module.exports = { * * @return {Promise} */ - async unreadCount(project, data, { user, ...context }) { + async unreadCount(project, _args, { factories, user, ...context }) { const eventsFactory = getEventsFactory(context, project._id); - const userInProject = new UserInProject(user.id, project._id); - const lastVisit = await userInProject.getLastVisit(); + const userModel = await factories.usersFactory.findById(user.id); + + if (!userModel) { + throw new ApolloError('User not found'); + } + const lastVisit = await userModel.getLastProjectVisit(project._id); return eventsFactory.getUnreadCount(lastVisit); },