Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions backend/core-api/src/commands/migrateContentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const command = async () => {
}

console.log(`Process finished at: ${new Date().toISOString()}`);

await client.close();
process.exit();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call client.close() before process.exit() for proper cleanup of the MongoDB connection.

Suggested change
process.exit();
await client.close(); process.exit();

};

command();
44 changes: 44 additions & 0 deletions backend/core-api/src/commands/migrateTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as dotenv from 'dotenv';

dotenv.config();

import { Collection, Db, MongoClient } from 'mongodb';

const { MONGO_URL = 'mongodb://localhost:27017/erxes?directConnection=true' } =
process.env;

if (!MONGO_URL) {
throw new Error(`Environment variable MONGO_URL not set.`);
}

const client = new MongoClient(MONGO_URL);

let db: Db;
let Tags: Collection;

const command = async () => {
await client.connect();
db = client.db() as Db;

Tags = db.collection('tags');

try {
await Tags.updateMany(
{},
{
$unset: { objectCount: '', order: '', scopeBrandIds: '' },
$set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Migration script sets parentId to empty string, which may not be valid for all usages.

Downstream code may expect parentId to be null or undefined when unset. Consider using null or omitting the field instead of assigning an empty string.

Suggested change
$set: { isGroup: false, parentId: '' }, // flatten parentId: no nested parent tags allowed
$set: { isGroup: false, parentId: null }, // flatten parentId: no nested parent tags allowed

},
);
} catch (e) {
console.log(`Error occurred: ${e.message}`);
await client.close();
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure client connection is closed on success too (e.g., in a finally block) to avoid leaving open connections.

console.log(`Process finished at: ${new Date().toISOString()}`);

await client.close();
process.exit();
};

command();
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const documentSchema = new Schema(
name: { type: String },
content: { type: String },
replacer: { type: String },
code: { type: String },
code: { type: String, optional: true },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Unique index on an optional field will cause duplicate-key errors; use a partial index and drop "optional".

  • MongoDB unique indexes on non-present/null values are tricky; with the current definition, inserts can collide on null/absent code. Also, Mongoose does not recognize "optional"; use required: false (or omit) and a partialFilterExpression to index only docs that have a code.

Apply:

-    code: { type: String, optional: true },
+    code: { type: String, required: false, trim: true },

-documentSchema.index({ code: 1 }, { unique: true });
+documentSchema.index(
+  { code: 1 },
+  { unique: true, partialFilterExpression: { code: { $exists: true } } }
+);

Verification (run in mongosh against the target DB):

Also applies to: 18-18


🏁 Script executed:

db.getCollection('documents').getIndexes()
# Expect the 'code_1' index to have { unique: true, partialFilterExpression: { code: { $exists: true } } }

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Print the 'code' field definition and surrounding lines
rg -n "code:\s*\{" -C2 backend/core-api/src/modules/documents/db/definitions/documents.ts
# Print all index definitions on documentSchema
rg -n "documentSchema\.index" -C3 backend/core-api/src/modules/documents/db/definitions/documents.ts

Length of output: 398


Use required:false (not optional) and add a partial filter to the unique ‘code’ index

Replace the field and index definitions as follows:

-    code: { type: String, optional: true },
+    code: { type: String, required: false, trim: true },

- documentSchema.index({ code: 1 }, { unique: true });
+ documentSchema.index(
+   { code: 1 },
+   { unique: true, partialFilterExpression: { code: { $exists: true } } }
+ );

After deployment, in the Mongo shell run:

db.getCollection('documents').getIndexes();

and confirm that the code_1 index includes your partialFilterExpression.

🤖 Prompt for AI Agents
In backend/core-api/src/modules/documents/db/definitions/documents.ts around
line 11, change the field definition from optional:true to required:false and
update the unique index on code to include a partialFilterExpression so the
unique constraint only applies when code exists (e.g. partialFilterExpression: {
code: { $exists: true } }), then deploy and run
db.getCollection('documents').getIndexes() in the Mongo shell to confirm the
code_1 index includes the partialFilterExpression.

},
{
timestamps: true,
Expand Down
1 change: 1 addition & 0 deletions backend/core-api/src/modules/tags/@types/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ITagFilterQueryParams
type: string;
tagIds?: string[];
parentId?: string;
isGroup?: boolean;
ids: string[];
excludeIds: boolean;
}
27 changes: 6 additions & 21 deletions backend/core-api/src/modules/tags/db/definitions/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,17 @@ export const tagSchema = schemaWrapper(
new Schema(
{
_id: mongooseStringRandomId,
name: { type: String, label: 'Name' },
type: {
type: String,
label: 'Type',
index: true,
},
name: { type: String, label: 'Name', unique: true },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Global unique on name is risky; scope uniqueness.

unique: true enforces cross-tenant/global uniqueness and blocks same-name tags under different groups. Prefer a compound unique index (name, parentId) or include tenant scope (e.g., organizationId).

-      name: { type: String, label: 'Name', unique: true },
+      name: { type: String, label: 'Name' },
@@
-tagSchema.index({ _id: 1, name: 1, parentId: 1 });
+tagSchema.index({ _id: 1, name: 1, parentId: 1 });
+// Enforce uniqueness only within the same parent (adjust to include tenant scope if applicable)
+tagSchema.index({ name: 1, parentId: 1 }, { unique: true });

If a unique index on name already exists in prod, we’ll need a migration to drop it before adding the compound index. I can prepare it.


🏁 Script executed:

#!/bin/bash
# Locate the tags.ts schema definition
fd -t f tags.ts

# Preview the top of the file to see field definitions
sed -n '1,200p' backend/core-api/src/modules/tags/db/definitions/tags.ts

# Inspect the schema object and index declarations
rg -n "tagSchema" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts
rg -n "index" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts

# Check for tenant or hierarchical scope fields
rg -n "parentId" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts
rg -n "organizationId" -C3 backend/core-api/src/modules/tags/db/definitions/tags.ts

Length of output: 2612


Scope tag name uniqueness to its parent (and tenant)

  • In backend/core-api/src/modules/tags/db/definitions/tags.ts (line 8), remove the field-level unique: true on name.
  • Add a compound unique index:
    tagSchema.index({ name: 1, parentId: 1 }, { unique: true });
    (Include tenant/organizationId in the index if this collection is multi-tenant.)
  • If a standalone unique index on name already exists in production, drop it via a migration before creating the new compound index.

colorCode: { type: String, label: 'Color code' },
objectCount: { type: Number, label: 'Object count' },
order: { type: String, label: 'Order', index: true },
parentId: {
type: String,
optional: true,
index: true,
label: 'Parent',
},
relatedIds: {
type: [String],
optional: true,
label: 'Children tag ids',
},
parentId: { type: String, label: 'Parent' },
relatedIds: { type: [String], label: 'Children tag ids' },
isGroup: { type: Boolean, label: 'Is group', default: false },
type: { type: String, label: 'Content type' },
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix ineffective indexes; add ones matching query patterns

The current compound index starts with _id, making it unusable for filtering by parentId, type, or isGroup. Reinstate targeted indexes for hot paths (tree traversal, type scoping, group filtering).

- tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 });
+ // Efficient lookups by hierarchy and content type
+ tagSchema.index({ parentId: 1 });
+ tagSchema.index({ type: 1, isGroup: 1 });
+ // Optional helper for search-by-name within a parent/type scope
+ tagSchema.index({ parentId: 1, type: 1, name: 1 });

Also applies to: 21-21

🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/db/definitions/tags.ts around lines 10-14
(and also apply same change at line 21), the existing compound index that starts
with _id is ineffective for queries filtering by parentId, type, or isGroup;
remove or stop relying on that index and add targeted indexes that match hot
query patterns: create a single-field index on parentId for tree traversal, a
compound index { parentId: 1, type: 1 } for scoped child lookups by content
type, a compound index { type: 1, isGroup: 1 } for fast group filtering within a
type, and add a multikey index on relatedIds if you query by child id; ensure
these indexes are defined in the schema/indexes section and avoid putting _id as
the leading key in compound indexes used for filtering.

{
timestamps: true,
},
),
);

// for tags query. increases search speed, avoids in-memory sorting
tagSchema.index({ _id: 1, type: 1, order: 1, name: 1, createdAt: 1 });
tagSchema.index({ _id: 1, name: 1, parentId: 1, type: 1 });
211 changes: 68 additions & 143 deletions backend/core-api/src/modules/tags/db/models/Tags.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { escapeRegExp } from 'erxes-api-shared/utils';
import { tagSchema } from '@/tags/db/definitions/tags';
import { removeRelatedTagIds, setRelatedTagIds } from '@/tags/utils';
import { ITag, ITagDocument } from 'erxes-api-shared/core-types';
import { Model } from 'mongoose';
import { IModels } from '~/connectionResolvers';
import { removeRelatedTagIds, setRelatedTagIds } from '@/tags/utils';
import { tagSchema } from '@/tags/db/definitions/tags';
export interface ITagModel extends Model<ITagDocument> {
getTag(_id: string): Promise<ITagDocument>;
createTag(doc: ITag): Promise<ITagDocument>;
Expand All @@ -13,107 +12,93 @@ export interface ITagModel extends Model<ITagDocument> {

export const loadTagClass = (models: IModels) => {
class Tag {
/*
* Get a tag
*/
public static async getTag(_id: string) {
const tag = await models.Tags.findOne({ _id });
public static async validate(_id: string | null, doc: ITag) {
const { name, parentId, isGroup } = doc;

if (!tag) {
throw new Error('Tag not found');
}
const tag = await models.Tags.findOne({
$or: [{ _id }, { name }],
});

return tag;
}
if (tag?.name === name) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uniqueness check for tag names does not exclude the current tag when updating, causing false duplicate errors. Adjust the check to compare tag._id with _id.

Suggested change
if (tag?.name === name) {
if (tag?.name === name && tag._id.toString() !== _id) {

throw new Error(`A tag named ${name} already exists`);
}

/**
* Create a tag
*/
public static async createTag(doc: ITag) {
const isUnique = await this.validateUniqueness(null, doc.name, doc.type);
if (tag?.isGroup && isGroup) {
throw new Error('Nested group is not allowed 1');
}

if (!isUnique) {
throw new Error('Tag duplicated');
if (_id === parentId) {
throw new Error('Group cannot be itself');
}

const parentTag = await this.getParentTag(doc);
if (parentId) {
const parentTag = await models.Tags.findOne({ _id: parentId });

// Generatingg order
const order = await this.generateOrder(parentTag, doc);
if (!parentTag) {
throw new Error('Group not found');
}

const tag = await models.Tags.create({
...doc,
order,
createdAt: new Date(),
});
if (!parentTag.isGroup) {
throw new Error('Parent tag must be a group');
}

await setRelatedTagIds(models, tag);
if ((isGroup || tag?.isGroup) && parentTag?.isGroup) {
throw new Error('Nested group is not allowed 2 ');
}
}

return tag;
}
if (tag) {
const parentTag = await models.Tags.findOne({ _id: tag.parentId });
const childTags = await models.Tags.find({ parentId: tag._id });

/**
* Update Tag
*/
public static async updateTag(_id: string, doc: ITag) {
const isUnique = await this.validateUniqueness(
{ _id },
doc.name,
doc.type,
);
if (parentTag?.isGroup && isGroup) {
throw new Error('Nested group is not allowed 3');
}

if (!isUnique) {
throw new Error('Tag duplicated');
if (!isGroup && childTags.length) {
throw new Error('Group has tags');
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix validation: duplicate-name check, parent/group rules, self-parent, and cycle prevention

Current logic mis-detects duplicates, conflates parent/group constraints, and can block benign edits (e.g., when isGroup is omitted). Consolidate validation using the effective next state, exclude self in duplicate checks, improve errors, and prevent cycles and self-parenting.

Apply:

-    public static async validate(_id: string | null, doc: ITag) {
-      const { name, parentId, isGroup } = doc;
-
-      const tag = await models.Tags.findOne({
-        $or: [{ _id }, { name }],
-      });
-
-      if (tag?.name === name) {
-        throw new Error(`A tag named ${name} already exists`);
-      }
-
-      if (tag?.isGroup && isGroup) {
-        throw new Error('Nested group is not allowed 1');
-      }
-
-      if (_id === parentId) {
-        throw new Error('Group cannot be itself');
-      }
-
-      if (parentId) {
-        const parentTag = await models.Tags.findOne({ _id: parentId });
-
-        if (!parentTag) {
-          throw new Error('Group not found');
-        }
-
-        if (!parentTag.isGroup) {
-          throw new Error('Parent tag must be a group');
-        }
-
-        if ((isGroup || tag?.isGroup) && parentTag?.isGroup) {
-          throw new Error('Nested group is not allowed 2 ');
-        }
-      }
-
-      if (tag) {
-        const parentTag = await models.Tags.findOne({ _id: tag.parentId });
-        const childTags = await models.Tags.find({ parentId: tag._id });
-
-        if (parentTag?.isGroup && isGroup) {
-          throw new Error('Nested group is not allowed 3');
-        }
-
-        if (!isGroup && childTags.length) {
-          throw new Error('Group has tags');
-        }
-      }
-    }
+    public static async validate(_id: string | null, doc: ITag) {
+      const { name, parentId, isGroup } = doc;
+
+      // 1) Unique name (exclude self on update)
+      const dup = await models.Tags.findOne({
+        name,
+        ...(!!_id ? { _id: { $ne: _id } } : {}),
+      });
+      if (dup) {
+        throw new Error(`A tag named ${name} already exists`);
+      }
+
+      // 2) Load existing on update and compute effective next state
+      const existing = _id ? await models.Tags.findOne({ _id }) : null;
+      const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup');
+      const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId');
+      const nextIsGroup = hasIsGroup ? !!isGroup : !!existing?.isGroup;
+      const nextParentId = hasParentId ? parentId : existing?.parentId;
+
+      // 3) Self-parent
+      if (_id && nextParentId && String(_id) === String(nextParentId)) {
+        throw new Error('A tag cannot be its own parent');
+      }
+
+      // 4) Parent existence + type + cycle
+      if (nextParentId) {
+        const parentTag = await models.Tags.findOne({ _id: nextParentId });
+        if (!parentTag) {
+          throw new Error('Parent tag not found');
+        }
+        if (!parentTag.isGroup) {
+          throw new Error('Parent tag must be a group');
+        }
+        // Prevent cycles: parent cannot be a descendant of current
+        if (_id && (parentTag.relatedIds || []).includes(_id)) {
+          throw new Error('Cannot set a tag’s parent to its descendant');
+        }
+      }
+
+      // 5) Group cannot have a parent
+      if (nextIsGroup && nextParentId) {
+        throw new Error('Group tag cannot have a parent');
+      }
+
+      if (existing) {
+        // 6) Disallow converting a nested tag into a group
+        if (!existing.isGroup && nextIsGroup && !!existing.parentId) {
+          throw new Error('Cannot convert a nested tag into a group');
+        }
+        // 7) Prevent demoting a group that already has children
+        if (existing.isGroup && !nextIsGroup) {
+          const childCount = await models.Tags.countDocuments({ parentId: _id });
+          if (childCount > 0) {
+            throw new Error(
+              'Cannot convert a group with children into a non-group',
+            );
+          }
+        }
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static async validate(_id: string | null, doc: ITag) {
const { name, parentId, isGroup } = doc;
if (!tag) {
throw new Error('Tag not found');
}
const tag = await models.Tags.findOne({
$or: [{ _id }, { name }],
});
return tag;
}
if (tag?.name === name) {
throw new Error(`A tag named ${name} already exists`);
}
/**
* Create a tag
*/
public static async createTag(doc: ITag) {
const isUnique = await this.validateUniqueness(null, doc.name, doc.type);
if (tag?.isGroup && isGroup) {
throw new Error('Nested group is not allowed 1');
}
if (!isUnique) {
throw new Error('Tag duplicated');
if (_id === parentId) {
throw new Error('Group cannot be itself');
}
const parentTag = await this.getParentTag(doc);
if (parentId) {
const parentTag = await models.Tags.findOne({ _id: parentId });
// Generatingg order
const order = await this.generateOrder(parentTag, doc);
if (!parentTag) {
throw new Error('Group not found');
}
const tag = await models.Tags.create({
...doc,
order,
createdAt: new Date(),
});
if (!parentTag.isGroup) {
throw new Error('Parent tag must be a group');
}
await setRelatedTagIds(models, tag);
if ((isGroup || tag?.isGroup) && parentTag?.isGroup) {
throw new Error('Nested group is not allowed 2 ');
}
}
return tag;
}
if (tag) {
const parentTag = await models.Tags.findOne({ _id: tag.parentId });
const childTags = await models.Tags.find({ parentId: tag._id });
/**
* Update Tag
*/
public static async updateTag(_id: string, doc: ITag) {
const isUnique = await this.validateUniqueness(
{ _id },
doc.name,
doc.type,
);
if (parentTag?.isGroup && isGroup) {
throw new Error('Nested group is not allowed 3');
}
if (!isUnique) {
throw new Error('Tag duplicated');
if (!isGroup && childTags.length) {
throw new Error('Group has tags');
}
}
}
public static async validate(_id: string | null, doc: ITag) {
const { name, parentId, isGroup } = doc;
// 1) Unique name (exclude self on update)
const dup = await models.Tags.findOne({
name,
...(!!_id ? { _id: { $ne: _id } } : {}),
});
if (dup) {
throw new Error(`A tag named ${name} already exists`);
}
// 2) Load existing on update and compute effective next state
const existing = _id ? await models.Tags.findOne({ _id }) : null;
const hasIsGroup = Object.prototype.hasOwnProperty.call(doc, 'isGroup');
const hasParentId = Object.prototype.hasOwnProperty.call(doc, 'parentId');
const nextIsGroup = hasIsGroup ? !!isGroup : !!existing?.isGroup;
const nextParentId = hasParentId ? parentId : existing?.parentId;
// 3) Self-parent guard
if (_id && nextParentId && String(_id) === String(nextParentId)) {
throw new Error('A tag cannot be its own parent');
}
// 4) Parent existence, type, and cycle prevention
if (nextParentId) {
const parentTag = await models.Tags.findOne({ _id: nextParentId });
if (!parentTag) {
throw new Error('Parent tag not found');
}
if (!parentTag.isGroup) {
throw new Error('Parent tag must be a group');
}
// Prevent setting a tag’s parent to its own descendant
if (_id && (parentTag.relatedIds || []).includes(_id)) {
throw new Error('Cannot set a tag’s parent to its descendant');
}
}
// 5) Disallow any parent on a group
if (nextIsGroup && nextParentId) {
throw new Error('Group tag cannot have a parent');
}
if (existing) {
// 6) Prevent converting a nested tag into a group
if (!existing.isGroup && nextIsGroup && !!existing.parentId) {
throw new Error('Cannot convert a nested tag into a group');
}
// 7) Prevent demoting a group that already has children
if (existing.isGroup && !nextIsGroup) {
const childCount = await models.Tags.countDocuments({ parentId: _id });
if (childCount > 0) {
throw new Error(
'Cannot convert a group with children into a non-group',
);
}
}
}
}
🤖 Prompt for AI Agents
In backend/core-api/src/modules/tags/db/models/Tags.ts around lines 15 to 62,
compute the effective next state (treat undefined isGroup as existing
tag.isGroup when editing) and then: 1) check for duplicate name excluding the
current _id (findOne({ name, _id: { $ne: _id } })); 2) ensure parentId !== _id
and walk the parent chain to prevent cycles (follow parentId up until null and
throw if any ancestor === _id); 3) verify parent exists and parent.isGroup ===
true; 4) forbid nested groups by disallowing a tag with isGroup === true to have
a parent that isGroup === true; 5) if changing/setting isGroup to false, ensure
there are no child tags (childTags.length === 0) before allowing the change; and
improve throw messages to be specific for each failure.


const parentTag = await this.getParentTag(doc);
public static async getTag(_id: string) {
const tag = await models.Tags.findOne({ _id });

if (parentTag && parentTag.parentId === _id) {
throw new Error('Cannot change tag');
if (!tag) {
throw new Error('Tag not found');
}

const tag = await models.Tags.getTag(_id);
return tag;
}

// Generatingg order
const order = await this.generateOrder(parentTag, doc);
public static async createTag(doc: ITag) {
await this.validate(null, doc);

const childTags = await models.Tags.find({
$and: [
{ order: { $regex: new RegExp(escapeRegExp(tag.order || ''), 'i') } },
{ _id: { $ne: _id } },
],
});
const tag = await models.Tags.create(doc);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Removing parentId from child tags on tag removal may leave orphaned tags.

Currently, child tags become orphaned when their parent is removed. Please review if this is intentional, or if child tags should be deleted or reassigned to maintain data integrity.

if (childTags.length > 0) {
const bulkDoc: Array<{
updateOne: {
filter: { _id: string };
update: { $set: { order: string } };
};
}> = [];
await setRelatedTagIds(models, tag);

// updating child tag order
childTags.forEach((childTag) => {
let childOrder = childTag.order || '';
return tag;
}

childOrder = childOrder.replace(tag.order || '', order);
public static async updateTag(_id: string, doc: ITag) {
await this.validate(_id, doc);

bulkDoc.push({
updateOne: {
filter: { _id: childTag._id },
update: { $set: { order: childOrder } },
},
});
});
const tag = await models.Tags.getTag(_id);

await models.Tags.bulkWrite(bulkDoc);
const childTags = await models.Tags.find({ parentId: tag._id });

if (childTags.length) {
await removeRelatedTagIds(models, tag);
}

await models.Tags.updateOne({ _id }, { $set: { ...doc, order } });

const updated = await models.Tags.findOne({ _id });
const updated = await models.Tags.findOneAndUpdate(
{ _id: tag._id },
doc,
{
new: true,
},
);

if (updated) {
await setRelatedTagIds(models, updated);
Expand All @@ -122,82 +107,22 @@ export const loadTagClass = (models: IModels) => {
return updated;
}

/**
* Remove Tag
*/
public static async removeTag(_id: string) {
const tag = await models.Tags.getTag(_id);

const childCount = await models.Tags.countDocuments({
parentId: _id,
});
const childTagIds = await models.Tags.find({ parentId: _id }).distinct(
'_id',
);

if (childCount > 0) {
throw new Error('Please remove child tags first');
}
await models.Tags.updateMany(
{ _id: { $in: childTagIds } },
{ $unset: { parentId: 1 } },
);

await removeRelatedTagIds(models, tag);

return models.Tags.deleteOne({ _id });
}

/*
* 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 models.Tags.countDocuments(selector);

if (selector && count > 1) {
return false;
}

const obj = selector && (await models.Tags.findOne(selector));

const filter: any = { name, type };

if (obj) {
filter._id = { $ne: obj._id };
}

const existing = await models.Tags.findOne(filter);

if (existing) {
return false;
}

return true;
}

/*
* Get a parent tag
*/
static async getParentTag(doc: ITag) {
return models.Tags.findOne({
_id: doc.parentId,
}).lean();
}

/**
* Generating order
*/
public static async generateOrder(
parentTag: ITagDocument | null,
{ name }: { name: string },
) {
const order = parentTag ? `${parentTag.order}${name}/` : `${name}/`;

return order;
}
}

tagSchema.loadClass(Tag);
Expand Down
14 changes: 0 additions & 14 deletions backend/core-api/src/modules/tags/graphql/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,4 @@ export const tagMutations = {
) {
return models.Tags.removeTag(_id);
},

/**
* Merge tags
*/
async tagsMerge(
_parent: undefined,
{ sourceId, destId }: { sourceId: string; destId: string },
{ models }: IContext,
) {
// remove old tag
await models.Tags.removeTag(sourceId);

return models.Tags.getTag(destId);
},
};
Loading