Skip to content

Commit

Permalink
Merge pull request #821 from Badsender-com/merge-staging-tags
Browse files Browse the repository at this point in the history
Merge staging tags
  • Loading branch information
omar-bear authored Jul 25, 2024
2 parents 63efd72 + 69bde17 commit 6f7dec7
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 52 deletions.
101 changes: 101 additions & 0 deletions migrate-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Connect to the MongoDB database
use badsender-development;

async function transformTags() {
try {
const startTime = new Date();
print(`Script started at: ${startTime}`);

// Step 1: Check if the tags collection exists
const collections = await db.getCollectionNames();
if (!collections.includes('tags')) {
await db.createCollection('tags');
print('Created tags collection.');
} else {
print('Tags collection already exists.');
}

// Step 2: Process emails in batches
const batchSize = 1000;
let skip = 0;
let emails;

while (true) {
const batchStartTime = new Date();
print(`Starting batch at: ${batchStartTime}`);
print(`Attempting to retrieve batch starting from ${skip}`);

// Retrieve a batch of emails
emails = await db.creations.find().skip(skip).limit(batchSize).toArray();
if (emails.length === 0) break;

print(`Retrieved ${emails.length} emails`);

// Step 3: Create a dictionary to store unique tags with their usageCount
const tagDict = {};

// Iterate over each email and each tag in the email to populate the dictionary
emails.forEach((email) => {
if (!Array.isArray(email.tags)) return;
email.tags.forEach((tag) => {
const key = `${tag}-${email._company}`;
if (!tagDict[key]) {
tagDict[key] = {
label: tag,
companyId: email._company,
usageCount: 0,
createdAt: new Date(),
updatedAt: new Date(),
};
}
tagDict[key].usageCount += 1;
tagDict[key].updatedAt = new Date();
});
});

print(`Created tag dictionary with ${Object.keys(tagDict).length} unique tags`);

// Step 4: Insert unique tags into the tags collection and get their IDs
for (const key of Object.keys(tagDict)) {
const tagData = tagDict[key];
try {
const existingTag = await db.tags.findOne({ label: tagData.label, companyId: tagData.companyId });
if (existingTag) {
await db.tags.updateOne(
{ _id: existingTag._id },
{
$set: {
usageCount: existingTag.usageCount + tagData.usageCount,
updatedAt: new Date(),
}
}
);
} else {
await db.tags.insertOne(tagData);
}
} catch (err) {
print(`Error processing tag ${key}: ${err}`);
}
}

print('Tag insertion and update completed');

const batchEndTime = new Date();
const batchDuration = (batchEndTime - batchStartTime) / 1000;
print(`Tag transformation completed for ${skip} to ${skip + batchSize} emails in ${batchDuration} seconds`);
skip += batchSize;
}

const endTime = new Date();
const totalDuration = (endTime - startTime) / 1000;
print(`Tag transformation completed successfully in ${totalDuration} seconds`);

} catch (error) {
print('Error during tag transformation:', error);
}
}

// Call the main function
transformTags().catch(error => {
print('Error during tag transformation:', error);
});
5 changes: 4 additions & 1 deletion packages/server/common/models.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const PersonalizedVariableSchema = require('../personalized-variables/personaliz
const FolderSchema = require('../folder/folder.schema');
const WorkspaceSchema = require('../workspace/workspace.schema');
const PersonalizedBlockSchema = require('../personalized-blocks/personalized-block-schema.js');

const TagSchema = require('../tag/tag.schema.js');
/// ///
// EXPORTS
/// ///
Expand Down Expand Up @@ -57,6 +57,8 @@ const OAuthClients = mongoose.model(
);
const OAuthCodes = mongoose.model(modelNames.OAuthCodes, OAuthCodesSchema);

const Tags = mongoose.model(modelNames.TagModel, TagSchema);

module.exports = {
mongoose,
// Compiled schema
Expand All @@ -75,4 +77,5 @@ module.exports = {
OAuthTokens,
OAuthClients,
OAuthCodes,
Tags,
};
6 changes: 6 additions & 0 deletions packages/server/constant/error-codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,10 @@ module.exports = {
BAD_FORMAT_PAGINATION: 'BAD_FORMAT_PAGINATION',
BAD_FORMAT_FILTERS: 'BAD_FORMAT_FILTERS',
UNEXPECTED_SERVER_ERROR: 'UNEXPECTED_SERVER_ERROR',

// TAGS
TAGS_NOT_FOUND: 'TAGS_NOT_FOUND',
TAG_NOT_FOUND: 'TAG_NOT_FOUND',
INVALID_TAG_DATA: 'INVALID_TAG_DATA',
INVALID_BULK_UPDATE_DATA: 'INVALID_DATA_FOR_BULK_UPDATE',
};
2 changes: 2 additions & 0 deletions packages/server/constant/model.names.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ module.exports = Object.freeze({
OAuthTokens: 'OAuthTokens',
OAuthClients: 'OAuthClients',
OAuthCodes: 'OAuthCodes',
// Tags
TagModel: 'Tag',
});
87 changes: 76 additions & 11 deletions packages/server/mailing/mailing.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const {
Forbidden,
} = require('http-errors');
const asyncHandler = require('express-async-handler');
const { Types } = require('mongoose');
const mongoose = require('mongoose');

const ERROR_CODES = require('../constant/error-codes.js');

const simpleI18n = require('../helpers/server-simple-i18n.js');
Expand All @@ -18,7 +19,6 @@ const {
downloadZip,
downloadMultipleZip,
} = require('./download-zip.controller.js');
const cleanTagName = require('../helpers/clean-tag-name.js');
const fileManager = require('../common/file-manage.service.js');
const modelsUtils = require('../utils/model.js');

Expand Down Expand Up @@ -271,7 +271,7 @@ async function rename(req, res) {
* @apiParam (Body) {String} mailingId
* @apiParam (Body) {String} workspaceId
* @apiParam (Body) {String} folderId
*
* @apiUse mailings
*/

Expand All @@ -283,6 +283,17 @@ async function copy(req, res) {

await mailingService.copyMailing(mailingId, { workspaceId, folderId }, user);

// Increment tag counts for the copied mailing
const originalMailing = await Mailings.findById(mailingId);
const tagLabels = originalMailing.tags;

if (tagLabels.length > 0) {
await mongoose.models.Tag.updateMany(
{ label: { $in: tagLabels }, companyId: originalMailing._company },
{ $inc: { usageCount: 1 } }
);
}

res.status(204).send();
}

Expand Down Expand Up @@ -432,37 +443,81 @@ async function updateMosaico(req, res) {

async function bulkUpdate(req, res) {
const { items, tags: tagsChanges = {} } = req.body;
const { id: companyId } = req.user._company;
const hadId = Array.isArray(items) && items.length;
const hasTagsChanges =
Array.isArray(tagsChanges.added) && Array.isArray(tagsChanges.removed);

if (!hadId || !hasTagsChanges) {
throw new UnprocessableEntity();
}

// Find existing tags in the database
const existingTags = await mongoose.models.Tag.find({
label: { $in: tagsChanges.added },
companyId,
}).lean();

const existingTagLabels = existingTags.map((tag) => tag.label);

// Separate new tags from existing ones
const newTags = tagsChanges.added.filter(
(tag) => !existingTagLabels.includes(tag)
);

// Create new tags in the database
if (newTags.length > 0) {
await mongoose.models.Tag.insertMany(
newTags.map((tag) => ({
label: tag,
companyId,
}))
);
}

const mailingQuery = modelsUtils.addStrictGroupFilter(req.user, {
_id: { $in: items.map(Types.ObjectId) },
_id: { $in: items.map(mongoose.Types.ObjectId) },
});
// ensure the mailings are from the same group

// Ensure mailings belong to the same group
const userMailings = await Mailings.find(mailingQuery).select({
_id: 1,
tags: 1,
});
const updateQueries = userMailings.map((mailing) => {
const { tags: orignalTags } = mailing;

const updateQueries = userMailings.map(async (mailing) => {
const originalTags = mailing.tags;
const uniqueUpdatedTags = [
...new Set([...tagsChanges.added, ...orignalTags]),
...new Set([...tagsChanges.added, ...originalTags]),
];
const updatedTags = uniqueUpdatedTags.filter(
(tag) => !tagsChanges.removed.includes(tag)
);
mailing.tags = updatedTags.map(cleanTagName).sort();

const tagsToAdd = updatedTags.filter((tag) => !originalTags.includes(tag));
const tagsToRemove = originalTags.filter(
(tag) => !updatedTags.includes(tag)
);

// Use schema methods to add and remove multiple tags
if (tagsToAdd.length > 0) {
await Mailings.addTagsToEmail(mailing._id, tagsToAdd);
}

if (tagsToRemove.length > 0) {
await Mailings.removeTagsFromEmail(mailing._id, tagsToRemove);
}

return mailing.save();
});

await Promise.all(updateQueries);

const [mailings, tags] = await Promise.all([
Mailings.findForApi(mailingQuery),
Mailings.findTags(modelsUtils.addStrictGroupFilter(req.user, {})),
]);

res.json({
meta: { tags },
items: mailings,
Expand All @@ -487,14 +542,14 @@ async function bulkDestroy(req, res) {
if (!Array.isArray(items) || !items.length) throw new UnprocessableEntity();

const mailingQuery = modelsUtils.addStrictGroupFilter(req.user, {
_id: { $in: items.map(Types.ObjectId) },
_id: { $in: items.map(mongoose.Types.ObjectId) },
});
// ensure the mailings are from the same group
const userMailings = await Mailings.find(mailingQuery)
.select({ _id: 1 })
.lean();
const safeMailingsIdList = userMailings.map((mailing) =>
Types.ObjectId(mailing._id)
mongoose.Types.ObjectId(mailing._id)
);
// Mongo responseFormat
// { n: 1, ok: 1, deletedCount: 1 }
Expand Down Expand Up @@ -530,6 +585,16 @@ async function deleteMailing(req, res) {
const { user } = req;
const { workspaceId, parentFolderId } = req.body;

// Find the email before deleting it
const mailing = await Mailings.findById(mailingId);
const tagLabels = mailing.tags;

// Decrement the tag count if the tag is used
if (tagLabels.length > 0) {
await Mailings.removeTagsFromEmail(mailingId, tagLabels);
}

// Delete the email
await mailingService.deleteMailing({
mailingId,
workspaceId,
Expand Down
Loading

0 comments on commit 6f7dec7

Please sign in to comment.