diff --git a/.circleci/config.yml b/.circleci/config.yml index 19092136..b91a021d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,7 +68,6 @@ workflows: branches: only: - dev - - feature/shapeup4-cqrs-update # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f1c52cb6..b1c22345 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -102,7 +102,7 @@ paths: schema: type: string default: id - enum: ["id", "createdAt", "startDate", "rateType", "status"] + enum: ["id", "createdAt", "updatedAt", "startDate", "rateType", "status"] description: The sort by column. - in: query name: sortOrder @@ -118,6 +118,24 @@ paths: schema: type: integer description: The project id. + - in: query + name: jobLocation + required: false + schema: + type: string + description: The location of the jobs. + - in: query + name: minSalary + required: false + schema: + type: integer + description: The minimum Salary. + - in: query + name: maxSalary + required: false + schema: + type: integer + description: The maximum Salary. - in: query name: isApplicationPageActive required: false @@ -4116,6 +4134,12 @@ components: description: "The user who updated the job last time.(Will get the user info from the token)" JobSearchBody: properties: + bodySkills: + type: array + items: + type: string + format: uuid + description: "The array of skill ids" jobIds: type: array items: @@ -4197,6 +4221,20 @@ components: type: string format: uuid description: "The role id." + showInHotList: + type: boolean + default: false + featured: + type: boolean + default: false + hotListExcerpt: + type: string + example: "This is very hot job" + description: "The further instruction to show for the hot job" + jobTag: + type: string + enum: ["", "new", "dollor", "hot"] + description: "The tag of a job" isApplicationPageActive: type: boolean default: false @@ -4739,6 +4777,20 @@ components: type: string format: uuid description: "The role id." + showInHotList: + type: boolean + default: false + featured: + type: boolean + default: false + hotListExcerpt: + type: string + example: "This is very hot job" + description: "The further instruction to show for the hot job" + jobTag: + type: string + enum: ["", "new", "dollor", "hot"] + description: "The tag of a job" isApplicationPageActive: type: boolean default: false diff --git a/migrations/2021-09-20-job-add-job-flag-fields.js b/migrations/2021-09-20-job-add-job-flag-fields.js new file mode 100644 index 00000000..b61c991b --- /dev/null +++ b/migrations/2021-09-20-job-add-job-flag-fields.js @@ -0,0 +1,45 @@ +const config = require('config') + +/* + * Add show_in_hot_list, featured, hot_list_excerpt and job_tag to the Job model. +type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false + */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'show_in_hot_list', + { type: Sequelize.BOOLEAN, allowNull: true, defaultValue: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'featured', + { type: Sequelize.BOOLEAN, allowNull: true, defaultValue: false }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hot_list_excerpt', + { type: Sequelize.STRING(255), allowNull: true, defaultValue: '' }, + { transaction }) + await queryInterface.addColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_tag', + { type: Sequelize.STRING(30), allowNull: true, defaultValue: '' }, + { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + }, + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'show_in_hot_list', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'featured', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'hot_list_excerpt', { transaction }) + await queryInterface.removeColumn({ tableName: 'jobs', schema: config.DB_SCHEMA_NAME }, 'job_tag', { transaction }) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + } +} diff --git a/src/bootstrap.js b/src/bootstrap.js index a8db1ad3..49652658 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -13,6 +13,7 @@ Joi.page = () => Joi.number().integer().min(1).default(1) Joi.perPage = () => Joi.number().integer().min(1).default(20) Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly', 'annual') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') +Joi.jobTag = () => Joi.string().valid('new', 'dollor', 'hot').allow('') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered', 'withdrawn', 'withdrawn-prescreen') diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index 606c690b..45140cff 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -58,7 +58,7 @@ async function deleteJob (req, res) { * @param res the response */ async function searchJobs (req, res) { - const query = { ...req.query, jobIds: _.get(req, 'body.jobIds', []) } + const query = { ...req.query, jobIds: _.get(req, 'body.jobIds', []), bodySkills: _.get(req, 'body.bodySkills', []) } const result = await service.searchJobs(req.authUser, query) helper.setResHeaders(req, res, result) res.send(result.result) diff --git a/src/models/Job.js b/src/models/Job.js index 46bb1fdd..90d9cf25 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -140,6 +140,30 @@ module.exports = (sequelize) => { type: Sequelize.UUID }) }, + showInHotList: { + field: 'show_in_hot_list', + type: Sequelize.BOOLEAN, + allowNull: true, + defaultValue: false + }, + featured: { + field: 'featured', + type: Sequelize.BOOLEAN, + allowNull: true, + defaultValue: false + }, + hotListExcerpt: { + field: 'hot_list_excerpt', + type: Sequelize.STRING(255), + allowNull: true, + defaultValue: '' + }, + jobTag: { + field: 'job_tag', + type: Sequelize.STRING(30), + allowNull: true, + defaultValue: '' + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/services/JobService.js b/src/services/JobService.js index 19a4cbd5..cdc22a72 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -231,7 +231,11 @@ createJob.schema = Joi.object() jobLocation: Joi.stringAllowEmpty().allow(null), jobTimezone: Joi.stringAllowEmpty().allow(null), currency: Joi.stringAllowEmpty().allow(null), - roleIds: Joi.array().items(Joi.string().uuid().required()) + roleIds: Joi.array().items(Joi.string().uuid().required()), + showInHotList: Joi.boolean().default(false), + featured: Joi.boolean().default(false), + hotListExcerpt: Joi.stringAllowEmpty().default(''), + jobTag: Joi.jobTag().default('') }) .required(), onTeamCreating: Joi.boolean().default(false) @@ -327,7 +331,11 @@ partiallyUpdateJob.schema = Joi.object() jobLocation: Joi.stringAllowEmpty().allow(null), jobTimezone: Joi.stringAllowEmpty().allow(null), currency: Joi.stringAllowEmpty().allow(null), - roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) + roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null), + showInHotList: Joi.boolean().default(false), + featured: Joi.boolean().default(false), + hotListExcerpt: Joi.stringAllowEmpty().default('').allow(null), + jobTag: Joi.jobTag().default('').allow(null) }) .required() }) @@ -367,7 +375,11 @@ fullyUpdateJob.schema = Joi.object().keys({ jobLocation: Joi.stringAllowEmpty().allow(null), jobTimezone: Joi.stringAllowEmpty().allow(null), currency: Joi.stringAllowEmpty().allow(null), - roleIds: Joi.array().items(Joi.string().uuid().required()).default(null) + roleIds: Joi.array().items(Joi.string().uuid().required()).default(null), + showInHotList: Joi.boolean().default(false), + featured: Joi.boolean().default(false), + hotListExcerpt: Joi.stringAllowEmpty().default('').allow(null), + jobTag: Joi.jobTag().default('').allow(null) }).required() }).required() @@ -444,7 +456,8 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } query: { bool: { must: [], - filter: [] + filter: [], + should: [] } }, from: (page - 1) * perPage, @@ -465,7 +478,9 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } 'rateType', 'workload', 'title', - 'status' + 'status', + 'minSalary', + 'maxSalary' ]), (value, key) => { let must if (key === 'description' || key === 'title') { @@ -482,6 +497,15 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [`${key}s`]: [value] } } + } else if (key === 'minSalary' || key === 'maxSalary') { + const salaryOp = key === 'minSalary' ? 'gte' : 'lte' + must = { + range: { + [key]: { + [salaryOp]: value + } + } + } } else { must = { term: { @@ -493,6 +517,27 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } } esQuery.body.query.bool.must.push(must) }) + // If criteria contains jobLocation, filter jobLocation with this value + if (criteria.jobLocation) { + // filter out null value + esQuery.body.query.bool.should.push({ + bool: { + must: [ + { + exists: { + field: 'jobLocation' + } + } + ] + } + }) + // filter the jobLocation + esQuery.body.query.bool.should.push({ + term: { + jobLocation: criteria.jobLocation + } + }) + } // If criteria contains projectIds, filter projectId with this value if (criteria.projectIds) { esQuery.body.query.bool.filter.push({ @@ -509,6 +554,14 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } } }) } + // if critera contains bodySkills, filter skills with this value + if (criteria.bodySkills && criteria.bodySkills.length > 0) { + esQuery.body.query.bool.filter.push({ + terms: { + skills: criteria.bodySkills + } + }) + } logger.debug({ component: 'JobService', context: 'searchJobs', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) @@ -555,9 +608,37 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } [Op.like]: `%${criteria.title}%` } } - if (criteria.skill) { - filter.skills = { - [Op.contains]: [criteria.skill] + if (criteria.jobLocation) { + filter.jobLocation = { + [Op.like]: `%${criteria.jobLocation}%` + } + } + if (criteria.skill || (criteria.bodySkills && criteria.bodySkills.length > 0)) { + const skill = criteria.skill + const bodySkills = criteria.bodySkills + if (skill && bodySkills && bodySkills.length > 0) { + filter.skills = { + [Op.and]: [ + { + [Op.contains]: [criteria.skill] + }, + { + [Op.or]: _.map(bodySkills, (item) => { + return { [Op.contains]: [item] } + }) + } + ] + } + } else if (skill) { + filter.skills = { + [Op.contains]: [criteria.skill] + } + } else if (bodySkills && bodySkills > 0) { + filter.skills = { + [Op.or]: _.map(bodySkills, (item) => { + return { [Op.contains]: [item] } + }) + } } } if (criteria.role) { @@ -568,6 +649,16 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } if (criteria.jobIds && criteria.jobIds.length > 0) { filter[Op.and].push({ id: criteria.jobIds }) } + if (criteria.minSalary !== undefined) { + filter.minSalary = { + [Op.gte]: criteria.minSalary + } + } + if (criteria.maxSalary !== undefined) { + filter.maxSalary = { + [Op.lte]: criteria.maxSalary + } + } const jobs = await Job.findAll({ where: filter, offset: ((page - 1) * perPage), @@ -594,7 +685,7 @@ searchJobs.schema = Joi.object().keys({ criteria: Joi.object().keys({ page: Joi.number().integer(), perPage: Joi.number().integer(), - sortBy: Joi.string().valid('id', 'createdAt', 'startDate', 'rateType', 'status'), + sortBy: Joi.string().valid('id', 'createdAt', 'updatedAt', 'startDate', 'rateType', 'status'), sortOrder: Joi.string().valid('desc', 'asc'), projectId: Joi.number().integer(), externalId: Joi.string(), @@ -609,7 +700,11 @@ searchJobs.schema = Joi.object().keys({ workload: Joi.workload(), status: Joi.jobStatus(), projectIds: Joi.array().items(Joi.number().integer()).single(), - jobIds: Joi.array().items(Joi.string().uuid()) + jobIds: Joi.array().items(Joi.string().uuid()), + bodySkills: Joi.array().items(Joi.string().uuid()), + minSalary: Joi.number().integer(), + maxSalary: Joi.number().integer(), + jobLocation: Joi.string() }).required(), options: Joi.object() }).required()