Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions convertors/set-user-project-last-visit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require('dotenv').config();
require('process');
const { MongoClient } = require('mongodb');

/**
* Method that runs convertor script
*/
async function run() {
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}`);

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;
let documentsUpdatedCount = 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);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.2.13",
"version": "1.2.14",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand All @@ -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",
Expand Down
40 changes: 38 additions & 2 deletions src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface UserNotificationsDBScheme {
/**
* Types of notifications to receive
*/
whatToReceive: {[key in UserNotificationType]: boolean};
whatToReceive: { [key in UserNotificationType]: boolean };
}

/**
Expand All @@ -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<string, number>;

/**
* User model
*/
Expand Down Expand Up @@ -130,6 +135,11 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
*/
public notifications!: UserNotificationsDBScheme;

/**
* User projects last visit
*/
public projectsLastVisit!: UserProjectsLastVisitDBScheme;

/**
* Saved bank cards for one-click payments
*/
Expand Down Expand Up @@ -233,6 +243,32 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
}
}

/**
* Update user's last project visit
*
* @param projectId - project id
*/
public async updateLastProjectVisit(projectId: string): Promise<number> {
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<number>}
*/
public async getLastProjectVisit(projectId: string): Promise<number> {
return this.projectsLastVisit?.[projectId] || 0;
}

/**
* Update user profile data
* @param user – user object
Expand Down Expand Up @@ -323,7 +359,7 @@ export default class UserModel extends AbstractModel<UserDBScheme> 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),
});
Expand Down
89 changes: 0 additions & 89 deletions src/models/userInProject.js

This file was deleted.

21 changes: 14 additions & 7 deletions src/resolvers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -358,10 +357,14 @@ module.exports = {
* @param {Context.user} user - current authorized user {@see ../index.js}
* @return {Promise<Number>}
*/
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: {
Expand Down Expand Up @@ -422,10 +425,14 @@ module.exports = {
*
* @return {Promise<number>}
*/
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);
},
Expand Down