diff --git a/convertors/set-user-project-last-visit.js b/convertors/set-user-project-last-visit.js index fb5b31ef..c841d8a6 100644 --- a/convertors/set-user-project-last-visit.js +++ b/convertors/set-user-project-last-visit.js @@ -35,7 +35,7 @@ async function run() { 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 } } }); + await hawkDb.collection('users').updateOne({ _id: userDocument._id }, { $set: { [`projectsLastVisit.${projectId}`]: userInProject.timestamp } }); usersUpdatedCount++; console.log(`Updated ${usersUpdatedCount}/${usersInProject.length} users in project ${collectionName}.`); } diff --git a/convertors/set-user-workspaces-membership.js b/convertors/set-user-workspaces-membership.js new file mode 100644 index 00000000..b216fd6e --- /dev/null +++ b/convertors/set-user-workspaces-membership.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 usersMembershipCollectionsToCheck = collections.filter(col => /^membership:/.test(col.name)).map(col => col.name); + + console.log(`Found ${usersMembershipCollectionsToCheck.length} users membership collections.`); + + const usersDocuments = await hawkDb.collection('users').find({}).toArray(); + + let i = 1; + + for (const collectionName of usersMembershipCollectionsToCheck) { + console.log(`[${i}/${usersMembershipCollectionsToCheck.length}] Processing ${collectionName}`); + + const userId = collectionName.split(':')[1]; + + const userDocument = usersDocuments.find(u => u._id.toString() === userId); + + if (!userDocument) { + i++; + continue; + } + + const memberships = await hawkDb.collection(collectionName).find({}).toArray(); + + for (const membership of memberships) { + const workspaceId = membership.workspaceId.toString(); + const isPending = membership.isPending || false; + await hawkDb.collection('users').updateOne({ _id: userDocument._id }, { $set: { [`workspaces.${workspaceId}`]: { isPending } } }); + } + + i++; + } + + await client.close(); +} + +run().catch(err => { + console.error('❌ Script failed:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/package.json b/package.json index 7e032b73..efcb5c1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.2.14", + "version": "1.2.18", "main": "index.ts", "license": "BUSL-1.1", "scripts": { diff --git a/src/models/user.ts b/src/models/user.ts index 1913e3ce..61489d49 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -33,22 +33,9 @@ export interface TokensPair { /** * Membership collection DB implementation */ -export interface MembershipDBScheme { - /** - * Document id - */ - _id: ObjectId; - - /** - * User's workspace id - */ - workspaceId: ObjectId; - - /** - * Shows if member is pending - */ +export type MembershipDBScheme = Record; /** * This structure represents how user notifications are stored at the DB (in 'users' collection) @@ -124,6 +111,11 @@ export default class UserModel extends AbstractModel implements Us */ public githubId?: string; + /** + * User's workspaces + */ + public workspaces!: MembershipDBScheme; + /** * User's original password (this field appears only after registration). * Using to send password to user after registration @@ -155,11 +147,6 @@ export default class UserModel extends AbstractModel implements Us */ protected collection: Collection; - /** - * Collection of user's workspaces - */ - private membershipCollection: Collection; - /** * Model constructor * @param modelData - user data @@ -174,7 +161,6 @@ export default class UserModel extends AbstractModel implements Us super(modelData); - this.membershipCollection = this.dbConnection.collection('membership:' + this._id); this.collection = this.dbConnection.collection('users'); } @@ -339,19 +325,13 @@ export default class UserModel extends AbstractModel implements Us * @param workspaceId - user's id to add * @param isPending - if true, mark user's membership as pending */ - public async addWorkspace(workspaceId: string, isPending = false): Promise { - const doc: OptionalId = { - workspaceId: new ObjectId(workspaceId), - }; - - if (isPending) { - doc.isPending = isPending; - } - - const documentId = (await this.membershipCollection.insertOne(doc)).insertedId; + public async addWorkspace(workspaceId: string, isPending = false): Promise<{ workspaceId: string }> { + await this.update( + { _id: new ObjectId(this._id) }, + { [`workspaces.${workspaceId}`]: { isPending } } + ); return { - id: documentId, workspaceId, }; } @@ -361,9 +341,10 @@ export default class UserModel extends AbstractModel implements Us * @param workspaceId - id of workspace to remove */ public async removeWorkspace(workspaceId: string): Promise<{ workspaceId: string }> { - await this.membershipCollection.deleteOne({ - workspaceId: new ObjectId(workspaceId), - }); + await this.collection.updateOne( + { _id: new ObjectId(this._id) }, + { $unset: { [`workspaces.${workspaceId}`]: '' } } + ); return { workspaceId, @@ -375,11 +356,9 @@ export default class UserModel extends AbstractModel implements Us * @param workspaceId - workspace id to confirm */ public async confirmMembership(workspaceId: string): Promise { - await this.membershipCollection.updateOne( - { - workspaceId: new ObjectId(workspaceId), - }, - { $unset: { isPending: '' } } + await this.collection.updateOne( + { _id: new ObjectId(this._id) }, + { $unset: { [`workspaces.${workspaceId}.isPending`]: '' } } ); } @@ -389,23 +368,22 @@ export default class UserModel extends AbstractModel implements Us * @param ids - workspaces id to filter them if there are workspaces that doesn't belong to the user */ public async getWorkspacesIds(ids: (string | ObjectId)[] = []): Promise { - const idsAsObjectId = ids.map(id => new ObjectId(id)); - const searchQuery = ids.length ? { - workspaceId: { - $in: idsAsObjectId, - }, - isPending: { - $ne: true, - }, - } : { - isPending: { - $ne: true, - }, - }; + const res = []; - const membershipDocuments = await this.membershipCollection.find(searchQuery).toArray(); + if (ids.length === 0) { + return Object.keys(this.workspaces); + } + + for (const id of ids) { + const workspaceId = id.toString(); + const workspace = this.workspaces[workspaceId]; + + if (workspace && workspace.isPending !== true) { + res.push(workspaceId); + } + } - return membershipDocuments.map(doc => doc.workspaceId.toString()); + return res; } /** diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 5cf0bb68..b79d30a1 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -14,6 +14,7 @@ const REPETITIONS_GROUP_HASH_INDEX_NAME = 'groupHash_hashed'; const REPETITIONS_USER_ID_INDEX_NAME = 'userId'; const EVENTS_TIMESTAMP_INDEX_NAME = 'timestamp'; const GROUPING_TIMESTAMP_INDEX_NAME = 'groupingTimestamp'; +const GROUPING_TIMESTAMP_AND_LAST_REPETITION_TIME_AND_ID_INDEX_NAME = 'groupingTimestampAndLastRepetitionTimeAndId'; const GROUPING_TIMESTAMP_AND_GROUP_HASH_INDEX_NAME = 'groupingTimestampAndGroupHash'; const MAX_SEARCH_QUERY_LENGTH = 50; @@ -117,6 +118,14 @@ module.exports = { name: GROUPING_TIMESTAMP_AND_GROUP_HASH_INDEX_NAME, }); + await projectDailyEventsCollection.createIndex({ + groupingTimestamp: -1, + lastRepetitionTime: -1, + _id: -1, + }, { + name: GROUPING_TIMESTAMP_AND_LAST_REPETITION_TIME_AND_ID_INDEX_NAME, + }); + await projectEventsCollection.createIndex({ groupHash: 1, },