Skip to content

Commit

Permalink
Merge pull request #99 from NIAEFEUP/feature/offers-search
Browse files Browse the repository at this point in the history
Added Search funcionality to GET /offers
  • Loading branch information
imnotteixeira authored Jan 11, 2021
2 parents 33d31f7 + 627e8b1 commit 083a142
Show file tree
Hide file tree
Showing 13 changed files with 514 additions and 84 deletions.
5 changes: 2 additions & 3 deletions src/api/middleware/validators/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { body, param, query } = require("express-validator");

const { useExpressValidators } = require("../errorHandler");
const ValidationReasons = require("./validationReasons");
const { checkDuplicatedEmail, valuesInSet } = require("./validatorUtils");
const { checkDuplicatedEmail, valuesInSet, ensureArray } = require("./validatorUtils");
const CompanyApplicationConstants = require("../../../models/constants/CompanyApplication");
const CompanyConstants = require("../../../models/constants/Company");
const AccountConstants = require("../../../models/constants/Account");
Expand Down Expand Up @@ -101,8 +101,7 @@ const search = useExpressValidators([
.isString().withMessage(ValidationReasons.STRING),
query("state", ValidationReasons.DEFAULT)
.optional()
.isJSON().withMessage(ValidationReasons.ARRAY).bail()
.customSanitizer(JSON.parse)
.customSanitizer(ensureArray)
.isArray().withMessage(ValidationReasons.ARRAY).bail()
.custom(valuesInSet(Object.keys(ApplicationStatus))),
query("submissionDateFrom", ValidationReasons.DEFAULT)
Expand Down
45 changes: 36 additions & 9 deletions src/api/middleware/validators/offer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ const { body, query } = require("express-validator");

const { useExpressValidators } = require("../errorHandler");
const ValidationReasons = require("./validationReasons");
const { valuesInSet } = require("./validatorUtils");
const { valuesInSet, ensureArray } = require("./validatorUtils");
const JobTypes = require("../../../models/constants/JobTypes");
const FieldTypes = require("../../../models/constants/FieldTypes");
const TechnologyTypes = require("../../../models/constants/TechnologyTypes");
const { FieldTypes, MIN_FIELDS, MAX_FIELDS } = require("../../../models/constants/FieldTypes");
const { TechnologyTypes, MIN_TECHNOLOGIES, MAX_TECHNOLOGIES } = require("../../../models/constants/TechnologyTypes");
const OfferService = require("../../../services/offer");
const OfferConstants = require("../../../models/constants/Offer");
const Company = require("../../../models/Company");
Expand Down Expand Up @@ -82,17 +82,17 @@ const create = useExpressValidators([

body("fields", ValidationReasons.DEFAULT)
.exists().withMessage(ValidationReasons.REQUIRED).bail()
.isArray({ min: FieldTypes.MIN_FIELDS, max: FieldTypes.MAX_FIELDS })
.withMessage(ValidationReasons.ARRAY_SIZE(FieldTypes.MIN_FIELDS, FieldTypes.MAX_FIELDS))
.isArray({ min: MIN_FIELDS, max: MAX_FIELDS })
.withMessage(ValidationReasons.ARRAY_SIZE(MIN_FIELDS, MAX_FIELDS))
.bail()
.custom(valuesInSet(FieldTypes.FieldTypes)),
.custom(valuesInSet(FieldTypes)),

body("technologies", ValidationReasons.DEFAULT)
.exists().withMessage(ValidationReasons.REQUIRED).bail()
.isArray({ min: TechnologyTypes.MIN_TECHNOLOGIES, max: TechnologyTypes.MAX_TECHNOLOGIES })
.withMessage(ValidationReasons.ARRAY_SIZE(TechnologyTypes.MIN_TECHNOLOGIES, TechnologyTypes.MAX_TECHNOLOGIES))
.isArray({ min: MIN_TECHNOLOGIES, max: MAX_TECHNOLOGIES })
.withMessage(ValidationReasons.ARRAY_SIZE(MIN_TECHNOLOGIES, MAX_TECHNOLOGIES))
.bail()
.custom(valuesInSet(TechnologyTypes.TechnologyTypes)),
.custom(valuesInSet(TechnologyTypes)),

body("isHidden", ValidationReasons.DEFAULT)
.optional()
Expand Down Expand Up @@ -144,6 +144,33 @@ const get = useExpressValidators([
.optional()
.isBoolean().withMessage(ValidationReasons.BOOLEAN)
.toBoolean(),

query("jobType")
.optional()
.isString().withMessage(ValidationReasons.STRING).bail()
.isIn(JobTypes).withMessage(ValidationReasons.IN_ARRAY(JobTypes)),

query("jobMinDuration", ValidationReasons.DEFAULT)
.optional()
.isInt().withMessage(ValidationReasons.INT).bail()
.toInt(),

query("jobMaxDuration", ValidationReasons.DEFAULT)
.optional()
.isInt().withMessage(ValidationReasons.INT).bail()
.toInt(),

query("fields", ValidationReasons.DEFAULT)
.optional()
.customSanitizer(ensureArray)
.isArray().withMessage(ValidationReasons.ARRAY).bail()
.custom(valuesInSet((FieldTypes))),

query("technologies", ValidationReasons.DEFAULT)
.optional()
.customSanitizer(ensureArray)
.isArray().withMessage(ValidationReasons.ARRAY).bail()
.custom(valuesInSet((TechnologyTypes))),
]);

module.exports = { create, get };
15 changes: 15 additions & 0 deletions src/api/middleware/validators/validatorUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,22 @@ const checkDuplicatedEmail = async (email) => {
}
};

/**
* Sanitizes the input val to return an array. If val is an array, this is a no-op
* Otherwise wraps val in an array
*
* This is especially helpful when you expect an array in a query param,
* but a one-element array is given, therefore it is parsed as a string instead
* @param {*} val
*/
const ensureArray = (val) => {
if (Array.isArray(val)) return val;

else return [val];
};

module.exports = {
valuesInSet,
checkDuplicatedEmail,
ensureArray
};
1 change: 1 addition & 0 deletions src/loaders/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const setupDbConnection = async () => {
dbName: config.db_name,
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false
};

try {
Expand Down
13 changes: 13 additions & 0 deletions src/models/Company.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const mongoose = require("mongoose");
const { Schema } = mongoose;
const CompanyConstants = require("./constants/Company");
const Offer = require("./Offer");

const CompanySchema = new Schema({
name: {
Expand All @@ -18,6 +19,18 @@ const CompanySchema = new Schema({
},
});

// Update offers from this company on name change
CompanySchema.post("findOneAndUpdate", async function(doc) {
await Offer.updateMany({ owner: doc._id }, { ownerName: doc.name });
});

// Delete Offers from the deleted Company (maybe we want to archive them instead?,
// also maybe we want other hooks as well such as deleteOne)
// CompanySchema.post("findOneAndDelete", async function(doc) {
// await Offer.deleteMany({ owner: doc._id }, { ownerName: doc.name });
// });

const Company = mongoose.model("Company", CompanySchema);


module.exports = Company;
13 changes: 5 additions & 8 deletions src/models/Offer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const JobTypes = require("./constants/JobTypes");
const { FieldTypes, MIN_FIELDS, MAX_FIELDS } = require("./constants/FieldTypes");
const { TechnologyTypes, MIN_TECHNOLOGIES, MAX_TECHNOLOGIES } = require("./constants/TechnologyTypes");
const PointSchema = require("./Point");
const Company = require("./Company");
const { MONTH_IN_MS, OFFER_MAX_LIFETIME_MONTHS } = require("./constants/TimeConstants");
const { noDuplicatesValidator, lengthBetweenValidator } = require("./modelUtils");
const OfferConstants = require("./constants/Offer");
Expand Down Expand Up @@ -76,18 +75,16 @@ const OfferSchema = new Schema({
default: false
},
owner: { type: Types.ObjectId, ref: "Company", required: true },
ownerName: { type: String, required: true },

location: { type: String, required: true },
coordinates: { type: PointSchema, required: false },
});

OfferSchema.methods.withCompany = async function() {
const offer = this.toObject();

const company = await Company.findById(offer.owner);

return { ...offer, company };
};
OfferSchema.index(
{ title: "text", ownerName: "text", jobType: "text", fields: "text", technologies: "text", location: "text" },
{ name: "Search index", weights: { title: 10, ownerName: 5, jobType: 5, location: 5, fields: 5, technologies: 5 } }
);

// Checking if the publication date is less than or equal than the end date.
function validatePublishDate(value) {
Expand Down
4 changes: 2 additions & 2 deletions src/models/constants/FieldTypes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const FieldTypes = Object.freeze([
"BACK_END",
"FRONT_END",
"BACKEND",
"FRONTEND",
"DEVOPS",
"BLOCKCHAIN",
"MACHINE LEARNING",
Expand Down
4 changes: 2 additions & 2 deletions src/models/constants/JobTypes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const JobTypes = Object.freeze([
"FULL_TIME",
"PART_TIME",
"FULL-TIME",
"PART-TIME",
"SUMMER INTERNSHIP",
"CURRICULAR INTERNSHIP",
"OTHER",
Expand Down
37 changes: 33 additions & 4 deletions src/services/offer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Company = require("../models/Company");
const Offer = require("../models/Offer");

class OfferService {
Expand Down Expand Up @@ -29,6 +30,8 @@ class OfferService {
location,
coordinates,
}) {

const ownerName = (await Company.findById(owner)).name;
const offer = await Offer.create({
title,
publishDate,
Expand All @@ -45,24 +48,50 @@ class OfferService {
technologies,
isHidden,
owner,
ownerName,
location,
coordinates,
});

return offer;
}

async get({ offset = 0, limit = OfferService.MAX_OFFERS_PER_QUERY, showHidden = false }) {
/**
* Fetches offers according to specified options
* @param {*} options
* value: Text to use in full-text-search
* offset: Point to start looking (and limiting)
* limit: How many offers to show
* jobType: Array of jobTypes allowed
*/
get({ value = "", offset = 0, limit = OfferService.MAX_OFFERS_PER_QUERY, showHidden = false, ...filters }) {

const offers = Offer.find().current();
const offers = value ? Offer.find(
{ "$and": [this._buildFilterQuery(filters), { "$text": { "$search": value } }] }, { score: { "$meta": "textScore" } }
) : Offer.find(this._buildFilterQuery(filters)).current();

if (!showHidden) offers.withoutHidden();

return Promise.all((await offers
return offers
.sort(value ? { score: { "$meta": "textScore" } } : undefined)
.skip(offset)
.limit(limit)
).map((o) => o.withCompany()));
;

}
_buildFilterQuery(filters) {
if (!filters || !Object.keys(filters).length) return {};

const { jobType, jobMinDuration, jobMaxDuration, fields, technologies } = filters;
const constraints = [];

if (jobType) constraints.push({ jobType: { "$in": jobType } });
if (jobMinDuration) constraints.push({ jobMinDuration: { "$gte": jobMinDuration } });
if (jobMaxDuration) constraints.push({ jobMaxDuration: { "$lte": jobMaxDuration } });
if (fields?.length) constraints.push({ fields: { "$elemMatch": { "$in": fields } } });
if (technologies?.length) constraints.push({ technologies: { "$elemMatch": { "$in": technologies } } });

return constraints.length ? { "$and": constraints } : {};
}
}

Expand Down
48 changes: 48 additions & 0 deletions test/company_schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const Company = require("../src/models/Company");
const SchemaTester = require("./utils/SchemaTester");
const CompanyConstants = require("../src/models/constants/Company");
const { DAY_TO_MS } = require("./utils/TimeConstants");
const Offer = require("../src/models/Offer");

const CompanySchemaTester = SchemaTester(Company);

Expand All @@ -16,4 +18,50 @@ describe("# Company Schema tests", () => {
CompanySchemaTester.maxLength("name", CompanyConstants.companyName.max_length);
});
});

describe("Hook tests", () => {

describe("Company name update", () => {

let company;

beforeAll(async () => {
company = await Company.create({
name: "first name"
});
const offer = {
title: "Test Offer",
publishDate: new Date(Date.now() - (DAY_TO_MS)),
publishEndDate: new Date(Date.now() + (DAY_TO_MS)),
description: "For Testing Purposes",
contacts: ["[email protected]", "229417766"],
jobType: "SUMMER INTERNSHIP",
fields: ["DEVOPS", "MACHINE LEARNING", "OTHER"],
technologies: ["React", "CSS"],
location: "Testing Street, Test City, 123",
owner: company._id,
ownerName: company.name
};

await Offer.create([offer, offer]);
});

afterAll(async () => {
await Offer.deleteMany({});
await Company.deleteMany({});
});

test("Should update offers ownerName on company name update", async () => {
const offersBefore = await Offer.find({ owner: company._id });

expect(offersBefore.every(({ ownerName }) => ownerName === "first name")).toBe(true);

await Company.findByIdAndUpdate(company._id, { name: "new name" }, { new: true });

const offersAfter = await Offer.find({ owner: company._id });

expect(offersAfter.every(({ ownerName }) => ownerName === "new name")).toBe(true);
});
});
});
});
Loading

0 comments on commit 083a142

Please sign in to comment.