Browse Source

refactor: added aggregation to remaining GET_DATA jobs and and getData actions, including userId to username filtering where possible

Kristian Vos 3 years ago
parent
commit
8987d36ae8

+ 54 - 16
backend/logic/actions/dataRequests.js

@@ -37,8 +37,13 @@ export default {
 
 		async.waterfall(
 			[
-				next => {
-					const newQueries = queries.map(query => {
+				// Creates pipeline array
+				next => next(null, []),
+
+				// Adds the match stage to aggregation pipeline, which is responsible for filtering
+				(pipeline, next) => {
+					let queryError;
+					const newQueries = queries.flatMap(query => {
 						const { data, filter, filterType } = query;
 						const newQuery = {};
 						if (filterType === "regex") {
@@ -65,8 +70,10 @@ export default {
 						} else if (filterType === "numberEquals") {
 							newQuery[filter.property] = { $eq: data };
 						}
+
 						return newQuery;
 					});
+					if (queryError) next(queryError);
 
 					const queryObject = {};
 					if (newQueries.length > 0) {
@@ -75,25 +82,56 @@ export default {
 						else if (operator === "nor") queryObject.$nor = newQueries;
 					}
 
-					next(null, queryObject);
+					pipeline.push({ $match: queryObject });
+
+					next(null, pipeline);
+				},
+
+				// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+				(pipeline, next) => {
+					const newSort = Object.fromEntries(
+						Object.entries(sort).map(([property, direction]) => [
+							property,
+							direction === "ascending" ? 1 : -1
+						])
+					);
+					if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+					next(null, pipeline);
+				},
+
+				// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+				(pipeline, next) => {
+					pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, next) => {
-					dataRequestModel.find(queryObject).count((err, count) => {
-						next(err, queryObject, count);
+				// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+				(pipeline, next) => {
+					pipeline.push({
+						$facet: {
+							count: [{ $count: "count" }],
+							documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+						}
 					});
+
+					// console.dir(pipeline, { depth: 6 });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, count, next) => {
-					dataRequestModel
-						.find(queryObject)
-						.sort(sort)
-						.skip(pageSize * (page - 1))
-						.limit(pageSize)
-						.select(properties.join(" "))
-						.exec((err, dataRequests) => {
-							next(err, count, dataRequests);
-						});
+				// Executes the aggregation pipeline
+				(pipeline, next) => {
+					dataRequestModel.aggregate(pipeline).exec((err, result) => {
+						// console.dir(err);
+						// console.dir(result, { depth: 6 });
+						if (err) return next(err);
+						if (result[0].count.length === 0) return next(null, 0, []);
+						const { count } = result[0].count[0];
+						const { documents } = result[0];
+						// console.log(111, err, result, count, documents[0]);
+						return next(null, count, documents);
+					});
 				}
 			],
 			async (err, count, dataRequests) => {

+ 117 - 16
backend/logic/actions/news.js

@@ -73,8 +73,73 @@ export default {
 
 		async.waterfall(
 			[
-				next => {
-					const newQueries = queries.map(query => {
+				// Creates pipeline array
+				next => next(null, []),
+
+				// If a filter exists for createdBy, add createdByUsername property to all documents
+				(pipeline, next) => {
+					// Check if a filter with the createdBy property exists
+					const createdByFilterExists =
+						queries.map(query => query.filter.property).indexOf("createdBy") !== -1;
+					// If no such filter exists, skip this function
+					if (!createdByFilterExists) return next(null, pipeline);
+
+					// Adds createdByOID field, which is an ObjectId version of createdBy
+					pipeline.push({
+						$addFields: {
+							createdByOID: {
+								$convert: {
+									input: "$createdBy",
+									to: "objectId",
+									onError: "unknown",
+									onNull: "unknown"
+								}
+							}
+						}
+					});
+
+					// Looks up user(s) with the same _id as the createdByOID and puts the result in the createdByUser field
+					pipeline.push({
+						$lookup: {
+							from: "users",
+							localField: "createdByOID",
+							foreignField: "_id",
+							as: "createdByUser"
+						}
+					});
+
+					// Unwinds the createdByUser array field into an object
+					pipeline.push({
+						$unwind: {
+							path: "$createdByUser",
+							preserveNullAndEmptyArrays: true
+						}
+					});
+
+					// Adds createdByUsername field from the createdByUser username, or unknown if it doesn't exist
+					pipeline.push({
+						$addFields: {
+							createdByUsername: {
+								$ifNull: ["$createdByUser.username", "unknown"]
+							}
+						}
+					});
+
+					// Removes the createdByOID and createdByUser property, just in case it doesn't get removed at a later stage
+					pipeline.push({
+						$project: {
+							createdByOID: 0,
+							createdByUser: 0
+						}
+					});
+
+					return next(null, pipeline);
+				},
+
+				// Adds the match stage to aggregation pipeline, which is responsible for filtering
+				(pipeline, next) => {
+					let queryError;
+					const newQueries = queries.flatMap(query => {
 						const { data, filter, filterType } = query;
 						const newQuery = {};
 						if (filterType === "regex") {
@@ -101,8 +166,13 @@ export default {
 						} else if (filterType === "numberEquals") {
 							newQuery[filter.property] = { $eq: data };
 						}
+
+						if (filter.property === "createdBy")
+							return { $or: [newQuery, { createdByUsername: newQuery.createdBy }] };
+
 						return newQuery;
 					});
+					if (queryError) next(queryError);
 
 					const queryObject = {};
 					if (newQueries.length > 0) {
@@ -111,25 +181,56 @@ export default {
 						else if (operator === "nor") queryObject.$nor = newQueries;
 					}
 
-					next(null, queryObject);
+					pipeline.push({ $match: queryObject });
+
+					next(null, pipeline);
+				},
+
+				// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+				(pipeline, next) => {
+					const newSort = Object.fromEntries(
+						Object.entries(sort).map(([property, direction]) => [
+							property,
+							direction === "ascending" ? 1 : -1
+						])
+					);
+					if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+					next(null, pipeline);
 				},
 
-				(queryObject, next) => {
-					newsModel.find(queryObject).count((err, count) => {
-						next(err, queryObject, count);
+				// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+				(pipeline, next) => {
+					pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+					next(null, pipeline);
+				},
+
+				// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+				(pipeline, next) => {
+					pipeline.push({
+						$facet: {
+							count: [{ $count: "count" }],
+							documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+						}
 					});
+
+					// console.dir(pipeline, { depth: 6 });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, count, next) => {
-					newsModel
-						.find(queryObject)
-						.sort(sort)
-						.skip(pageSize * (page - 1))
-						.limit(pageSize)
-						.select(properties.join(" "))
-						.exec((err, news) => {
-							next(err, count, news);
-						});
+				// Executes the aggregation pipeline
+				(pipeline, next) => {
+					newsModel.aggregate(pipeline).exec((err, result) => {
+						// console.dir(err);
+						// console.dir(result, { depth: 6 });
+						if (err) return next(err);
+						if (result[0].count.length === 0) return next(null, 0, []);
+						const { count } = result[0].count[0];
+						const { documents } = result[0];
+						// console.log(111, err, result, count, documents[0]);
+						return next(null, count, documents);
+					});
 				}
 			],
 			async (err, count, news) => {

+ 183 - 16
backend/logic/actions/punishments.js

@@ -44,8 +44,137 @@ export default {
 
 		async.waterfall(
 			[
-				next => {
-					const newQueries = queries.map(query => {
+				// Creates pipeline array
+				next => next(null, []),
+
+				// If a filter exists for value, add valueUsername property to all documents
+				(pipeline, next) => {
+					// Check if a filter with the value property exists
+					const valueFilterExists =
+						queries.map(query => query.filter.property).indexOf("value") !== -1;
+					// If no such filter exists, skip this function
+					if (!valueFilterExists) return next(null, pipeline);
+
+					// Adds valueOID field, which is an ObjectId version of value
+					pipeline.push({
+						$addFields: {
+							valueOID: {
+								$convert: {
+									input: "$value",
+									to: "objectId",
+									onError: "unknown",
+									onNull: "unknown"
+								}
+							}
+						}
+					});
+
+					// Looks up user(s) with the same _id as the valueOID and puts the result in the valueUser field
+					pipeline.push({
+						$lookup: {
+							from: "users",
+							localField: "valueOID",
+							foreignField: "_id",
+							as: "valueUser"
+						}
+					});
+
+					// Unwinds the valueUser array field into an object
+					pipeline.push({
+						$unwind: {
+							path: "$valueUser",
+							preserveNullAndEmptyArrays: true
+						}
+					});
+
+					// Adds valueUsername field from the valueUser username, or unknown if it doesn't exist, or Musare if it's set to Musare
+					pipeline.push({
+						$addFields: {
+							valueUsername: {
+								$cond: [
+									{ $eq: [ "$type", "banUserId" ] },
+									{ $ifNull: ["$valueUser.username", "unknown"] },
+									null
+								]
+							}
+						}
+					});
+
+					// Removes the valueOID and valueUser property, just in case it doesn't get removed at a later stage
+					pipeline.push({
+						$project: {
+							valueOID: 0,
+							valueUser: 0
+						}
+					});
+
+					return next(null, pipeline);
+				},
+
+				// If a filter exists for punishedBy, add punishedByUsername property to all documents
+				(pipeline, next) => {
+					// Check if a filter with the punishedBy property exists
+					const punishedByFilterExists =
+						queries.map(query => query.filter.property).indexOf("punishedBy") !== -1;
+					// If no such filter exists, skip this function
+					if (!punishedByFilterExists) return next(null, pipeline);
+
+					// Adds punishedByOID field, which is an ObjectId version of punishedBy
+					pipeline.push({
+						$addFields: {
+							punishedByOID: {
+								$convert: {
+									input: "$punishedBy",
+									to: "objectId",
+									onError: "unknown",
+									onNull: "unknown"
+								}
+							}
+						}
+					});
+
+					// Looks up user(s) with the same _id as the punishedByOID and puts the result in the punishedByUser field
+					pipeline.push({
+						$lookup: {
+							from: "users",
+							localField: "punishedByOID",
+							foreignField: "_id",
+							as: "punishedByUser"
+						}
+					});
+
+					// Unwinds the punishedByUser array field into an object
+					pipeline.push({
+						$unwind: {
+							path: "$punishedByUser",
+							preserveNullAndEmptyArrays: true
+						}
+					});
+
+					// Adds punishedByUsername field from the punishedByUser username, or unknown if it doesn't exist
+					pipeline.push({
+						$addFields: {
+							punishedByUsername: {
+								$ifNull: ["$punishedByUser.username", "unknown"]
+							}
+						}
+					});
+
+					// Removes the punishedByOID and punishedByUser property, just in case it doesn't get removed at a later stage
+					pipeline.push({
+						$project: {
+							punishedByOID: 0,
+							punishedByUser: 0
+						}
+					});
+
+					return next(null, pipeline);
+				},
+
+				// Adds the match stage to aggregation pipeline, which is responsible for filtering
+				(pipeline, next) => {
+					let queryError;
+					const newQueries = queries.flatMap(query => {
 						const { data, filter, filterType } = query;
 						const newQuery = {};
 						if (filterType === "regex") {
@@ -72,8 +201,15 @@ export default {
 						} else if (filterType === "numberEquals") {
 							newQuery[filter.property] = { $eq: data };
 						}
+
+						if (filter.property === "value")
+							return { $or: [newQuery, { valueUsername: newQuery.value }] };
+						if (filter.property === "punishedBy")
+							return { $or: [newQuery, { punishedByUsername: newQuery.punishedBy }] };
+
 						return newQuery;
 					});
+					if (queryError) next(queryError);
 
 					const queryObject = {};
 					if (newQueries.length > 0) {
@@ -82,25 +218,56 @@ export default {
 						else if (operator === "nor") queryObject.$nor = newQueries;
 					}
 
-					next(null, queryObject);
+					pipeline.push({ $match: queryObject });
+
+					next(null, pipeline);
+				},
+
+				// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+				(pipeline, next) => {
+					const newSort = Object.fromEntries(
+						Object.entries(sort).map(([property, direction]) => [
+							property,
+							direction === "ascending" ? 1 : -1
+						])
+					);
+					if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+					next(null, pipeline);
+				},
+
+				// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+				(pipeline, next) => {
+					pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, next) => {
-					punishmentModel.find(queryObject).count((err, count) => {
-						next(err, queryObject, count);
+				// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+				(pipeline, next) => {
+					pipeline.push({
+						$facet: {
+							count: [{ $count: "count" }],
+							documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+						}
 					});
+
+					// console.dir(pipeline, { depth: 6 });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, count, next) => {
-					punishmentModel
-						.find(queryObject)
-						.sort(sort)
-						.skip(pageSize * (page - 1))
-						.limit(pageSize)
-						.select(properties.join(" "))
-						.exec((err, punishments) => {
-							next(err, count, punishments);
-						});
+				// Executes the aggregation pipeline
+				(pipeline, next) => {
+					punishmentModel.aggregate(pipeline).exec((err, result) => {
+						// console.dir(err);
+						// console.dir(result, { depth: 6 });
+						if (err) return next(err);
+						if (result[0].count.length === 0) return next(null, 0, []);
+						const { count } = result[0].count[0];
+						const { documents } = result[0];
+						// console.log(111, err, result, count, documents[0]);
+						return next(null, count, documents);
+					});
 				}
 			],
 			async (err, count, punishments) => {

+ 117 - 16
backend/logic/actions/reports.js

@@ -76,8 +76,73 @@ export default {
 
 		async.waterfall(
 			[
-				next => {
-					const newQueries = queries.map(query => {
+				// Creates pipeline array
+				next => next(null, []),
+
+				// If a filter exists for createdBy, add createdByUsername property to all documents
+				(pipeline, next) => {
+					// Check if a filter with the createdBy property exists
+					const createdByFilterExists =
+						queries.map(query => query.filter.property).indexOf("createdBy") !== -1;
+					// If no such filter exists, skip this function
+					if (!createdByFilterExists) return next(null, pipeline);
+
+					// Adds createdByOID field, which is an ObjectId version of createdBy
+					pipeline.push({
+						$addFields: {
+							createdByOID: {
+								$convert: {
+									input: "$createdBy",
+									to: "objectId",
+									onError: "unknown",
+									onNull: "unknown"
+								}
+							}
+						}
+					});
+
+					// Looks up user(s) with the same _id as the createdByOID and puts the result in the createdByUser field
+					pipeline.push({
+						$lookup: {
+							from: "users",
+							localField: "createdByOID",
+							foreignField: "_id",
+							as: "createdByUser"
+						}
+					});
+
+					// Unwinds the createdByUser array field into an object
+					pipeline.push({
+						$unwind: {
+							path: "$createdByUser",
+							preserveNullAndEmptyArrays: true
+						}
+					});
+
+					// Adds createdByUsername field from the createdByUser username, or unknown if it doesn't exist
+					pipeline.push({
+						$addFields: {
+							createdByUsername: {
+								$ifNull: ["$createdByUser.username", "unknown"]
+							}
+						}
+					});
+
+					// Removes the createdByOID and createdByUser property, just in case it doesn't get removed at a later stage
+					pipeline.push({
+						$project: {
+							createdByOID: 0,
+							createdByUser: 0
+						}
+					});
+
+					return next(null, pipeline);
+				},
+
+				// Adds the match stage to aggregation pipeline, which is responsible for filtering
+				(pipeline, next) => {
+					let queryError;
+					const newQueries = queries.flatMap(query => {
 						const { data, filter, filterType } = query;
 						const newQuery = {};
 						if (filterType === "regex") {
@@ -104,8 +169,13 @@ export default {
 						} else if (filterType === "numberEquals") {
 							newQuery[filter.property] = { $eq: data };
 						}
+
+						if (filter.property === "createdBy")
+							return { $or: [newQuery, { createdByUsername: newQuery.createdBy }] };
+
 						return newQuery;
 					});
+					if (queryError) next(queryError);
 
 					const queryObject = {};
 					if (newQueries.length > 0) {
@@ -114,25 +184,56 @@ export default {
 						else if (operator === "nor") queryObject.$nor = newQueries;
 					}
 
-					next(null, queryObject);
+					pipeline.push({ $match: queryObject });
+
+					next(null, pipeline);
+				},
+
+				// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+				(pipeline, next) => {
+					const newSort = Object.fromEntries(
+						Object.entries(sort).map(([property, direction]) => [
+							property,
+							direction === "ascending" ? 1 : -1
+						])
+					);
+					if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+					next(null, pipeline);
+				},
+
+				// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+				(pipeline, next) => {
+					pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, next) => {
-					reportModel.find(queryObject).count((err, count) => {
-						next(err, queryObject, count);
+				// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+				(pipeline, next) => {
+					pipeline.push({
+						$facet: {
+							count: [{ $count: "count" }],
+							documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+						}
 					});
+
+					// console.dir(pipeline, { depth: 6 });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, count, next) => {
-					reportModel
-						.find(queryObject)
-						.sort(sort)
-						.skip(pageSize * (page - 1))
-						.limit(pageSize)
-						.select(properties.join(" "))
-						.exec((err, reports) => {
-							next(err, count, reports);
-						});
+				// Executes the aggregation pipeline
+				(pipeline, next) => {
+					reportModel.aggregate(pipeline).exec((err, result) => {
+						// console.dir(err);
+						// console.dir(result, { depth: 6 });
+						if (err) return next(err);
+						if (result[0].count.length === 0) return next(null, 0, []);
+						const { count } = result[0].count[0];
+						const { documents } = result[0];
+						// console.log(111, err, result, count, documents[0]);
+						return next(null, count, documents);
+					});
 				}
 			],
 			async (err, count, reports) => {

+ 79 - 38
backend/logic/actions/users.js

@@ -185,9 +185,13 @@ export default {
 
 		async.waterfall(
 			[
-				next => {
+				// Creates pipeline array
+				next => next(null, []),
+
+				// Adds the match stage to aggregation pipeline, which is responsible for filtering
+				(pipeline, next) => {
 					let queryError;
-					const newQueries = queries.map(query => {
+					const newQueries = queries.flatMap(query => {
 						const { data, filter, filterType } = query;
 						const newQuery = {};
 						if (filterType === "regex") {
@@ -214,6 +218,7 @@ export default {
 						} else if (filterType === "numberEquals") {
 							newQuery[filter.property] = { $eq: data };
 						}
+
 						return newQuery;
 					});
 					if (queryError) next(queryError);
@@ -225,48 +230,84 @@ export default {
 						else if (operator === "nor") queryObject.$nor = newQueries;
 					}
 
-					next(null, queryObject);
-				},
-
-				(queryObject, next) => {
-					const invalidProperties = [...properties, ...queries.map(query => query.filter.property)].find(
-						property => {
-							if (
-								[
-									"services.password",
-									"services.password.password",
-									"services.password.reset.code",
-									"services.password.reset.expires",
-									"services.password.set.code",
-									"services.password.set.expires",
-									"services.github.access_token",
-									"services.email.verificationToken"
-								].includes(property)
-							)
-								return true;
-							return false;
-						}
+					pipeline.push({ $match: queryObject });
+
+					next(null, pipeline);
+				},
+
+				// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+				(pipeline, next) => {
+					const newSort = Object.fromEntries(
+						Object.entries(sort).map(([property, direction]) => [
+							property,
+							direction === "ascending" ? 1 : -1
+						])
 					);
-					if (invalidProperties) next("Invalid paramaters given.");
-					else next(null, queryObject);
+					if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+					next(null, pipeline);
+				},
+
+				// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+				(pipeline, next) => {
+					pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, next) => {
-					userModel.find(queryObject).count((err, count) => {
-						next(err, queryObject, count);
+				// Adds second project stage to aggregation pipeline, responsible for excluding some specific properties
+				(pipeline, next) => {
+					pipeline.push({
+						$project: {
+							"services.password.password": 0,
+							"services.password.reset.code": 0,
+							"services.password.reset.expires": 0,
+							"services.password.set.code": 0,
+							"services.password.set.expires": 0,
+							"services.github.access_token": 0,
+							"email.verificationToken": 0,
+						}
+					});
+
+					// [
+					// 	"services.password",
+					// 	"services.password.password",
+					// 	"services.password.reset.code",
+					// 	"services.password.reset.expires",
+					// 	"services.password.set.code",
+					// 	"services.password.set.expires",
+					// 	"services.github.access_token",
+					// 	"services.email.verificationToken"
+					// ]
+
+					next(null, pipeline);
+				},
+
+				// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+				(pipeline, next) => {
+					pipeline.push({
+						$facet: {
+							count: [{ $count: "count" }],
+							documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+						}
 					});
+
+					// console.dir(pipeline, { depth: 6 });
+
+					next(null, pipeline);
 				},
 
-				(queryObject, count, next) => {
-					userModel
-						.find(queryObject)
-						.sort(sort)
-						.skip(pageSize * (page - 1))
-						.limit(pageSize)
-						.select(properties.join(" "))
-						.exec((err, users) => {
-							next(err, count, users);
-						});
+				// Executes the aggregation pipeline
+				(pipeline, next) => {
+					userModel.aggregate(pipeline).exec((err, result) => {
+						// console.dir(err);
+						console.dir(result, { depth: 6 });
+						if (err) return next(err);
+						if (result[0].count.length === 0) return next(null, 0, []);
+						const { count } = result[0].count[0];
+						const { documents } = result[0];
+						// console.log(111, err, result, count, documents[0]);
+						return next(null, count, documents);
+					});
 				}
 			],
 			async (err, count, users) => {

+ 126 - 18
backend/logic/playlists.js

@@ -879,11 +879,81 @@ class _PlaylistsModule extends CoreClass {
 		return new Promise((resolve, reject) => {
 			async.waterfall(
 				[
-					next => {
+					// Creates pipeline array
+					next => next(null, []),
+
+					// If a filter exists for createdBy, add createdByUsername property to all documents
+					(pipeline, next) => {
+						const { queries } = payload;
+
+						// Check if a filter with the createdBy property exists
+						const createdByFilterExists =
+							queries.map(query => query.filter.property).indexOf("createdBy") !== -1;
+						// If no such filter exists, skip this function
+						if (!createdByFilterExists) return next(null, pipeline);
+
+						// Adds createdByOID field, which is an ObjectId version of createdBy
+						pipeline.push({
+							$addFields: {
+								createdByOID: {
+									$convert: {
+										input: "$createdBy",
+										to: "objectId",
+										onError: "unknown",
+										onNull: "unknown"
+									}
+								}
+							}
+						});
+
+						// Looks up user(s) with the same _id as the createdByOID and puts the result in the createdByUser field
+						pipeline.push({
+							$lookup: {
+								from: "users",
+								localField: "createdByOID",
+								foreignField: "_id",
+								as: "createdByUser"
+							}
+						});
+
+						// Unwinds the createdByUser array field into an object
+						pipeline.push({
+							$unwind: {
+								path: "$createdByUser",
+								preserveNullAndEmptyArrays: true
+							}
+						});
+
+						// Adds createdByUsername field from the createdByUser username, or unknown if it doesn't exist, or Musare if it's set to Musare
+						pipeline.push({
+							$addFields: {
+								createdByUsername: {
+									$cond: [
+										{ $eq: [ "$createdBy", "Musare" ] },
+										"Musare",
+										{ $ifNull: ["$createdByUser.username", "unknown"] }
+									]
+								}
+							}
+						});
+
+						// Removes the createdByOID and createdByUser property, just in case it doesn't get removed at a later stage
+						pipeline.push({
+							$project: {
+								createdByOID: 0,
+								createdByUser: 0
+							}
+						});
+
+						return next(null, pipeline);
+					},
+
+					// Adds the match stage to aggregation pipeline, which is responsible for filtering
+					(pipeline, next) => {
 						const { queries, operator } = payload;
 
 						let queryError;
-						const newQueries = queries.map(query => {
+						const newQueries = queries.flatMap(query => {
 							const { data, filter, filterType } = query;
 							const newQuery = {};
 							if (filterType === "regex") {
@@ -910,6 +980,10 @@ class _PlaylistsModule extends CoreClass {
 							} else if (filterType === "numberEquals") {
 								newQuery[filter.property] = { $eq: data };
 							}
+
+							if (filter.property === "createdBy")
+								return { $or: [newQuery, { createdByUsername: newQuery.createdBy }] };
+
 							return newQuery;
 						});
 						if (queryError) next(queryError);
@@ -921,27 +995,61 @@ class _PlaylistsModule extends CoreClass {
 							else if (operator === "nor") queryObject.$nor = newQueries;
 						}
 
-						next(null, queryObject);
+						pipeline.push({ $match: queryObject });
+
+						next(null, pipeline);
+					},
+
+					// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+					(pipeline, next) => {
+						const { sort } = payload;
+						const newSort = Object.fromEntries(
+							Object.entries(sort).map(([property, direction]) => [
+								property,
+								direction === "ascending" ? 1 : -1
+							])
+						);
+						if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+						next(null, pipeline);
+					},
+
+					// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+					(pipeline, next) => {
+						const { properties } = payload;
+
+						pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+						next(null, pipeline);
 					},
 
-					(queryObject, next) => {
-						PlaylistsModule.playlistModel.find(queryObject).count((err, count) => {
-							next(err, queryObject, count);
+					// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+					(pipeline, next) => {
+						const { page, pageSize } = payload;
+
+						pipeline.push({
+							$facet: {
+								count: [{ $count: "count" }],
+								documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+							}
 						});
+
+						// console.dir(pipeline, { depth: 6 });
+
+						next(null, pipeline);
 					},
 
-					(queryObject, count, next) => {
-						const { page, pageSize, properties, sort } = payload;
-
-						PlaylistsModule.playlistModel
-							.find(queryObject)
-							.sort(sort)
-							.skip(pageSize * (page - 1))
-							.limit(pageSize)
-							.select(properties.join(" "))
-							.exec((err, playlists) => {
-								next(err, count, playlists);
-							});
+					// Executes the aggregation pipeline
+					(pipeline, next) => {
+						PlaylistsModule.playlistModel.aggregate(pipeline).exec((err, result) => {
+							// console.dir(err);
+							// console.dir(result, { depth: 6 });
+							if (err) return next(err);
+							if (result[0].count.length === 0) return next(null, 0, []);
+							const { count } = result[0].count[0];
+							const { documents } = result[0];
+							// console.log(111, err, result, count, documents[0]);
+							return next(null, count, documents);
+						});
 					}
 				],
 				(err, count, playlists) => {

+ 127 - 19
backend/logic/stations.js

@@ -403,15 +403,85 @@ class _StationsModule extends CoreClass {
 	 * @param {string} payload.operator - the operator for queries
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
-	GET_DATA(payload) {
+	 GET_DATA(payload) {
 		return new Promise((resolve, reject) => {
 			async.waterfall(
 				[
-					next => {
+					// Creates pipeline array
+					next => next(null, []),
+
+					// If a filter exists for owner, add ownerUsername property to all documents
+					(pipeline, next) => {
+						const { queries } = payload;
+
+						// Check if a filter with the owner property exists
+						const ownerFilterExists =
+							queries.map(query => query.filter.property).indexOf("owner") !== -1;
+						// If no such filter exists, skip this function
+						if (!ownerFilterExists) return next(null, pipeline);
+
+						// Adds ownerOID field, which is an ObjectId version of owner
+						pipeline.push({
+							$addFields: {
+								ownerOID: {
+									$convert: {
+										input: "$owner",
+										to: "objectId",
+										onError: "unknown",
+										onNull: "unknown"
+									}
+								}
+							}
+						});
+
+						// Looks up user(s) with the same _id as the ownerOID and puts the result in the ownerUser field
+						pipeline.push({
+							$lookup: {
+								from: "users",
+								localField: "ownerOID",
+								foreignField: "_id",
+								as: "ownerUser"
+							}
+						});
+
+						// Unwinds the ownerUser array field into an object
+						pipeline.push({
+							$unwind: {
+								path: "$ownerUser",
+								preserveNullAndEmptyArrays: true
+							}
+						});
+
+						// Adds ownerUsername field from the ownerUser username, if owner doesn't exist then it's none, or if user/username doesn't exist then it's unknown
+						pipeline.push({
+							$addFields: {
+								ownerUsername: {
+									$cond: [
+										{ $eq: [ { $type: "$owner" }, "string" ] },
+										{ $ifNull: ["$ownerUser.username", "unknown"] },
+										"none"
+									]
+								}
+							}
+						});
+
+						// Removes the ownerOID and ownerUser property, just in case it doesn't get removed at a later stage
+						pipeline.push({
+							$project: {
+								ownerOID: 0,
+								ownerUser: 0
+							}
+						});
+
+						return next(null, pipeline);
+					},
+
+					// Adds the match stage to aggregation pipeline, which is responsible for filtering
+					(pipeline, next) => {
 						const { queries, operator } = payload;
 
 						let queryError;
-						const newQueries = queries.map(query => {
+						const newQueries = queries.flatMap(query => {
 							const { data, filter, filterType } = query;
 							const newQuery = {};
 							if (filterType === "regex") {
@@ -438,6 +508,10 @@ class _StationsModule extends CoreClass {
 							} else if (filterType === "numberEquals") {
 								newQuery[filter.property] = { $eq: data };
 							}
+
+							if (filter.property === "owner")
+								return { $or: [newQuery, { ownerUsername: newQuery.owner }] };
+
 							return newQuery;
 						});
 						if (queryError) next(queryError);
@@ -449,27 +523,61 @@ class _StationsModule extends CoreClass {
 							else if (operator === "nor") queryObject.$nor = newQueries;
 						}
 
-						next(null, queryObject);
+						pipeline.push({ $match: queryObject });
+
+						next(null, pipeline);
 					},
 
-					(queryObject, next) => {
-						StationsModule.stationModel.find(queryObject).count((err, count) => {
-							next(err, queryObject, count);
+					// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+					(pipeline, next) => {
+						const { sort } = payload;
+						const newSort = Object.fromEntries(
+							Object.entries(sort).map(([property, direction]) => [
+								property,
+								direction === "ascending" ? 1 : -1
+							])
+						);
+						if (Object.keys(newSort).length > 0) pipeline.push({ $sort: newSort });
+						next(null, pipeline);
+					},
+
+					// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+					(pipeline, next) => {
+						const { properties } = payload;
+
+						pipeline.push({ $project: Object.fromEntries(properties.map(property => [property, 1])) });
+
+						next(null, pipeline);
+					},
+
+					// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+					(pipeline, next) => {
+						const { page, pageSize } = payload;
+
+						pipeline.push({
+							$facet: {
+								count: [{ $count: "count" }],
+								documents: [{ $skip: pageSize * (page - 1) }, { $limit: pageSize }]
+							}
 						});
+
+						// console.dir(pipeline, { depth: 6 });
+
+						next(null, pipeline);
 					},
 
-					(queryObject, count, next) => {
-						const { page, pageSize, properties, sort } = payload;
-
-						StationsModule.stationModel
-							.find(queryObject)
-							.sort(sort)
-							.skip(pageSize * (page - 1))
-							.limit(pageSize)
-							.select(properties.join(" "))
-							.exec((err, stations) => {
-								next(err, count, stations);
-							});
+					// Executes the aggregation pipeline
+					(pipeline, next) => {
+						StationsModule.stationModel.aggregate(pipeline).exec((err, result) => {
+							// console.dir(err);
+							// console.dir(result, { depth: 6 });
+							if (err) return next(err);
+							if (result[0].count.length === 0) return next(null, 0, []);
+							const { count } = result[0].count[0];
+							const { documents } = result[0];
+							// console.log(111, err, result, count, documents[0]);
+							return next(null, count, documents);
+						});
 					}
 				],
 				(err, count, stations) => {