api/src/db/models/Tags.ts
import { Model, model } from 'mongoose';
import * as _ from 'underscore';
import {
Companies,
Conversations,
Customers,
EngageMessages,
Integrations,
Products,
ProductTemplates
} from '.';
import { ACTIVITY_LOG_ACTIONS, putActivityLog } from '../../data/logUtils';
import { escapeRegExp } from '../../data/utils';
import { ITag, ITagDocument, tagSchema } from './definitions/tags';
import { IUserDocument } from './definitions/users';
interface ITagObjectParams {
type: string;
tagIds: string[];
targetIds: string[];
}
// set related tags
const setRelatedIds = async (tag: ITagDocument) => {
if (tag.parentId) {
const parentTag = await Tags.findOne({ _id: tag.parentId });
if (parentTag) {
let relatedIds: string[];
relatedIds = tag.relatedIds || [];
relatedIds.push(tag._id);
relatedIds = _.union(relatedIds, parentTag.relatedIds || []);
await Tags.updateOne({ _id: parentTag._id }, { $set: { relatedIds } });
const updated = await Tags.findOne({ _id: tag.parentId });
if (updated) {
await setRelatedIds(updated);
}
}
}
};
// remove related tags
const removeRelatedIds = async (tag: ITagDocument) => {
const tags = await Tags.find({ relatedIds: { $in: tag._id } });
if (tags.length === 0) {
return;
}
const relatedIds: string[] = tag.relatedIds || [];
relatedIds.push(tag._id);
const doc: Array<{
updateOne: {
filter: { _id: string };
update: { $set: { relatedIds: string[] } };
};
}> = [];
tags.forEach(async t => {
const ids = (t.relatedIds || []).filter(id => !relatedIds.includes(id));
doc.push({
updateOne: {
filter: { _id: t._id },
update: { $set: { relatedIds: ids } }
}
});
});
await Tags.bulkWrite(doc);
};
export const getCollection = type => {
let collection: any = Conversations;
switch (type) {
case 'customer':
collection = Customers;
break;
case 'engageMessage':
collection = EngageMessages;
break;
case 'company':
collection = Companies;
break;
case 'integration':
collection = Integrations;
break;
case 'product':
collection = Products;
break;
case 'productTemplate':
collection = ProductTemplates;
break;
}
return collection;
};
export interface ITagModel extends Model<ITagDocument> {
getTag(_id: string): Promise<ITagDocument>;
merge(sourceId: string, destId: string): Promise<ITagDocument>;
createTag(doc: ITag): Promise<ITagDocument>;
updateTag(_id: string, doc: ITag): Promise<ITagDocument>;
removeTag(_id: string): void;
tagObject(params: ITagObjectParams, user?: IUserDocument): void;
validateUniqueness(
selector: any,
name: string,
type: string
): Promise<boolean>;
}
export const loadClass = () => {
class Tag {
/*
* Get a tag
*/
public static async getTag(_id: string) {
const tag = await Tags.findOne({ _id });
if (!tag) {
throw new Error('Tag not found');
}
return tag;
}
/*
* Validates tag uniquness
*/
public static async validateUniqueness(
selector: any,
name: string,
type: string
): Promise<boolean> {
// required name and type
if (!name || !type) {
return true;
}
// can't update name & type same time more than one tags.
const count = await Tags.find(selector).countDocuments();
if (selector && count > 1) {
return false;
}
const obj = selector && (await Tags.findOne(selector));
const filter: any = { name, type };
if (obj) {
filter._id = { $ne: obj._id };
}
const existing = await Tags.findOne(filter);
if (existing) {
return false;
}
return true;
}
/*
* Common helper for taggable objects like conversation, engage, customer etc ...
*/
public static async tagObject(
{ type, tagIds, targetIds }: ITagObjectParams,
user?: IUserDocument
) {
const collection = getCollection(type);
const prevTagsCount = await Tags.find({
_id: { $in: tagIds },
type
}).countDocuments();
if (prevTagsCount !== tagIds.length) {
throw new Error('Tag not found.');
}
await collection.updateMany(
{ _id: { $in: targetIds } },
{ $set: { tagIds } },
{ multi: true }
);
const targets = await collection.find({ _id: { $in: targetIds } }).lean();
for (const target of targets) {
await putActivityLog({
action: ACTIVITY_LOG_ACTIONS.CREATE_TAG_LOG,
data: {
contentId: target._id,
userId: user ? user._id : '',
contentType: type,
target,
content: { tagIds: tagIds || [] }
}
});
}
}
/*
* Get a parent tag
*/
static async getParentTag(doc: ITag) {
return Tags.findOne({
_id: doc.parentId
}).lean();
}
/**
* Create a tag
*/
public static async createTag(doc: ITag) {
const isUnique = await Tags.validateUniqueness(null, doc.name, doc.type);
if (!isUnique) {
throw new Error('Tag duplicated');
}
const parentTag = await this.getParentTag(doc);
// Generatingg order
const order = await this.generateOrder(parentTag, doc);
const tag = await Tags.create({
...doc,
order,
createdAt: new Date()
});
await setRelatedIds(tag);
return tag;
}
/**
* Update Tag
*/
public static async updateTag(_id: string, doc: ITag) {
const isUnique = await Tags.validateUniqueness(
{ _id },
doc.name,
doc.type
);
if (!isUnique) {
throw new Error('Tag duplicated');
}
const parentTag = await this.getParentTag(doc);
if (parentTag && parentTag.parentId === _id) {
throw new Error('Cannot change tag');
}
// Generatingg order
const order = await this.generateOrder(parentTag, doc);
const tag = await Tags.findOne({
_id
});
if (tag && tag.order) {
const childTags = await Tags.find({
$and: [
{ order: { $regex: new RegExp(escapeRegExp(tag.order), 'i') } },
{ _id: { $ne: _id } }
]
});
if (childTags.length > 0) {
const bulkDoc: Array<{
updateOne: {
filter: { _id: string };
update: { $set: { order: string } };
};
}> = [];
// updating child categories order
childTags.forEach(async childTag => {
let childOrder = childTag.order;
if (tag.order && childOrder) {
childOrder = childOrder.replace(tag.order, order);
bulkDoc.push({
updateOne: {
filter: { _id: childTag._id },
update: { $set: { order: childOrder } }
}
});
}
});
await Tags.bulkWrite(bulkDoc);
await removeRelatedIds(tag);
}
}
await Tags.updateOne({ _id }, { $set: { ...doc, order } });
const updated = await Tags.findOne({ _id });
if (updated) {
await setRelatedIds(updated);
}
return updated;
}
/**
* Remove Tag
*/
public static async removeTag(_id: string) {
const tag = await Tags.getTag(_id);
const collection = getCollection(tag.type);
const childCount = await Tags.countDocuments({ parentId: _id });
if (childCount > 0) {
throw new Error('Please remove child tags first');
}
await collection.updateMany(
{ tagIds: { $in: [_id] } },
{ $pull: { tagIds: { $in: [_id] } } }
);
await removeRelatedIds(tag);
return Tags.deleteOne({ _id });
}
/**
* Generating order
*/
public static async generateOrder(
parentTag: ITagDocument,
{ name, type }: { name: string; type: string }
) {
const order = `${name}${type}`;
if (!parentTag) {
return order;
}
let parentOrder = parentTag.order;
if (!parentOrder) {
parentOrder = `${parentTag.name}${parentTag.type}`;
await Tags.updateOne(
{
_id: parentTag._id
},
{ $set: { order: parentOrder } }
);
}
return `${parentOrder}/${order}`;
}
public static async merge(
sourceId: string,
destId: string
): Promise<ITagDocument> {
const source = await Tags.getTag(sourceId);
const collection = await getCollection(source.type);
const items = await collection.find(
{ tagIds: { $in: [sourceId] } },
{ _id: 1 }
);
const itemIds = items.map(i => i._id);
// add to new destination
await collection.updateMany(
{ _id: { $in: itemIds } },
{ $push: { tagIds: [destId] } }
);
// remove old tag
await Tags.removeTag(sourceId);
return Tags.getTag(destId);
}
}
tagSchema.loadClass(Tag);
return tagSchema;
};
loadClass();
// tslint:disable-next-line
const Tags = model<ITagDocument, ITagModel>('tags', tagSchema);
export default Tags;