diff --git a/controllers/doorLock.js b/controllers/doorLock.js index 3e57137..7520b3c 100644 --- a/controllers/doorLock.js +++ b/controllers/doorLock.js @@ -1,6 +1,7 @@ 'use strict'; -const { parseDoorLockDataFile, writeDoorLockEvent } = require("../services/doorLock"); +const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources'); +const { parseDoorLockDataFile, writeDoorLockEvent } = require('../services/doorLock'); const { fetchAllBookings, writeBookingReservation } = require('../services/officeRnD/bookings'); const { officeRnDAPIErrors } = require('../constants/constants'); @@ -8,16 +9,16 @@ const IncomingForm = require('formidable').IncomingForm; const uploadDoorLockData = (req, res) => { const form = new IncomingForm(); - const parsingResults = []; + const fileParsers = []; form.on('file', (field, file) => { if (file && file.type === 'text/csv') { - parsingResults.push(parseDoorLockDataFile(file)); + fileParsers.push(parseDoorLockDataFile(file)); } }); form.on('end', () => { - Promise.all(parsingResults) + Promise.all(fileParsers) .then((parserResults) => { const parsedData = []; const parserErrors = []; @@ -48,7 +49,7 @@ const uploadDoorLockData = (req, res) => { writeDoorLockEvent(entry); }); }) - .catch(() => { + .catch((error) => { res.status(500).send(officeRnDAPIErrors.FAILED_TO_FETCH_MEMBERS); }); }); @@ -56,6 +57,37 @@ const uploadDoorLockData = (req, res) => { form.parse(req); }; +const getKnownOfficeResourceMappings = (req, res) => { + const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ]; + + Promise.all(dataToFetch) + .then(result => { + res.send({ + existingMappings: result[0], + offices: result[1], + resources: result[2], + }); + }) + .catch(error => { + res.status(500).send(); + }); +}; + +const addNewMapping = (req, res) => { + const newMapping = req.body && req.body.mapping ? req.body.mapping : null; + if (newMapping && newMapping.officeSlug && newMapping.resourceSlug && newMapping.officeId && newMapping.resourceId){ + saveNewMappingToDatabase(newMapping) + .then(result => { + res.send(newMapping); + }) + .catch(error => { + res.status(500).send(error); + }); + } +}; + module.exports = { uploadDoorLockData, + getKnownOfficeResourceMappings, + addNewMapping, }; diff --git a/routes/index.js b/routes/index.js index f5b4e2c..e787596 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,12 +1,15 @@ 'use strict'; const { apiStatusCheck } = require('../controllers/apiStatusCheck'); -const { uploadDoorLockData } = require('../controllers/doorLock'); +const { uploadDoorLockData, getKnownOfficeResourceMappings, addNewMapping } = require('../controllers/doorLock'); const express = require('express'); const router = express.Router(); router.get('/', apiStatusCheck); + router.post('/doorLock/upload', uploadDoorLockData); +router.get('/doorLock/mappings', getKnownOfficeResourceMappings); +router.post('/doorLock/mappings', addNewMapping); module.exports = router; diff --git a/server.js b/server.js index bbb332e..ff8a27d 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,8 @@ const app = express(); const port = process.env.PORT || 5000; +app.use(express.json()); + app.use('/api', routes); app.use(basicAuth({ diff --git a/services/doorLock.js b/services/doorLock.js index 497ae66..0ae78c4 100644 --- a/services/doorLock.js +++ b/services/doorLock.js @@ -9,14 +9,29 @@ const { USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE, - USER_UNLOCKED_DOOR, - USER_LOCKED_DOOR, VALID_CSV_HEADERS, + doorLockEvents, csvParserErrors, } = require('../constants/constants'); const { fetchAllMembers, findMember } = require('../services/officeRnD/members'); +const { getMappingsFromDatabase } = require('../services/officeRnD/resources'); +const extractMappingFromFileName = (fileName) => { + const contentBetweenBracketsRegex = /\[(.*?)\]/; + const rawContent = fileName.match(contentBetweenBracketsRegex)[1]; + const mappingContent = rawContent.split('-').map(word => word.trim()); + return { + officeSlug: mappingContent[0], + resourceSlug: mappingContent[1], + } +}; + +const checkIfMappingExsists = (mappingFromFileName, mappings) => { + const { officeSlug, resourceSlug } = mappingFromFileName; + + return mappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug)); +}; const parseDoorLockDataFile = (file) => { return new Promise ((resolve, reject) => { @@ -25,6 +40,134 @@ const parseDoorLockDataFile = (file) => { const unknownMembers = []; let isValidFile = true; + const prefetchDataJobs = [getMappingsFromDatabase(), fetchAllMembers()]; + + Promise.all(prefetchDataJobs) + .then(result => { + const mappings = result[0]; + + const mappingFromFileName = extractMappingFromFileName(file.name); + const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings); + if (!mappingObject){ + reject('Error ! File contains unknown location'); + return; + } + + fs.createReadStream(file.path) + .pipe(csv({ + mapHeaders: ({ header, index }) => header.trim().toLowerCase(), + mapValues: ({ header, index, value }) => value.trim() + })) + .on('headers', (headers) => { + + const sortedValidHeadersArray = VALID_CSV_HEADERS.concat().sort(); + const sortedParsedHeadersArray = headers.map(header => header.trim()).sort(); + + let validHeaders = true; + if (sortedParsedHeadersArray.length !== sortedValidHeadersArray.length) { + validHeaders = false; + }else { + for (let i = 0; i < sortedValidHeadersArray.length; i++){ + validHeaders = validHeaders + && (sortedValidHeadersArray[i] === sortedParsedHeadersArray[i]); + } + } + + if (!validHeaders){ + isValidFile = false; + errors.push({ + error: csvParserErrors.INVALID_HEADERS, + details: `Expected headers : ${JSON.stringify(VALID_CSV_HEADERS)}`, + file: file.name, + }); + } + }) + .on('data', (data) => { + if (!isValidFile) { + return; + } + + const eventType = data.event.trim(); + if ([USER_ENTRY_EVENT, ENABLE_PASSAGE_MODE, DISABLE_PASSAGE_MODE].includes(eventType)){ + results.push(data); + } + }) + .on('end', () => { + const parsedData = []; + let i = 0; + while (i < results.length){ + //Verify pair + //First entry type should be user entry and second should be enable / disable passage + const firstEntry = results[i]; + const secondEntry = results[i+1]; + + if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){ + const memberObject = findMember(firstEntry.name); + + if (!memberObject){ + //Check if member is already labeled as unknown + const unknownMember = unknownMembers.find((member) => member.details === firstEntry.name); + if (!unknownMember){ + unknownMembers.push({ + error: csvParserErrors.UNKNOWN_MEMBER, + details: firstEntry.name, + file: file.name, + }); + } + } + + if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){ + const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ? + doorLockEvents.USER_UNLOCKED : doorLockEvents.USER_LOCKED; + + const dateTimeString = `${firstEntry.date} ${firstEntry.time}`; + const timestamp = moment.utc(dateTimeString, 'MM/DD/YY HH:mm:ss A').toISOString(); + + //Verify that member is registered in OfficeRnD system + if (memberObject){ + const entryData = { + memberName: firstEntry.name, + memberNumber: firstEntry['user no'], + memberId: memberObject.memberId, + timestamp, + event, + resourceId: mappingObject.resourceId, + + }; + + parsedData.push(entryData); + } + i+=2; + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE, + details: firstEntry, + file: file.name, + }); + i+=1; + } + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER, + details: firstEntry, + file: file.name, + }); + i+=1; + } + } + resolve({ + parsedData, + unknownMembers, + errors + }); + }); + + }) + .catch(error => { + reject(error); + }); + + /* fetchAllMembers() .then(() => { fs.createReadStream(file.path) @@ -135,13 +278,15 @@ const parseDoorLockDataFile = (file) => { .catch((error) => { reject(error); }); + */ }); }; const writeDoorLockEvent = (entry) => { - db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}}) - .then() - .catch(); + console.log('Write entry : '); + console.log(entry); + console.log('===='); + return db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}}); }; module.exports = { diff --git a/services/officeRnD/bookings.js b/services/officeRnD/bookings.js index ad834bd..7fe57e8 100644 --- a/services/officeRnD/bookings.js +++ b/services/officeRnD/bookings.js @@ -15,7 +15,7 @@ const fetchAllBookings = () => { cleanedBookingReservations.push({ reservationId: fullBookingEntry['_id'], memberId: fullBookingEntry.member, - resource: fullBookingEntry.resourceId, + resourceId: fullBookingEntry.resourceId, start: fullBookingEntry.start.dateTime, end: fullBookingEntry.end.dateTime, }); @@ -29,9 +29,7 @@ const fetchAllBookings = () => { }; const writeBookingReservation = (bookingReservation) => { - db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}}) - .then() - .catch(); + return db.bookingReservation.findOrCreate({where: {...bookingReservation}, defaults: {...bookingReservation}}); }; module.exports = { diff --git a/services/officeRnD/resources.js b/services/officeRnD/resources.js new file mode 100644 index 0000000..95b1d64 --- /dev/null +++ b/services/officeRnD/resources.js @@ -0,0 +1,61 @@ +'use strict'; + +const db = require('../../models/index'); + +const { API } = require('../../helpers/api'); + +const fetchOffices = () => { + return new Promise((resolve, reject) => { + API.get('/offices') + .then((result) => { + const offices = result.data || []; + const cleanedOffices = []; + offices.forEach(office => { + cleanedOffices.push({ + officeId: office['_id'], + officeName: office.name, + }); + }); + resolve(cleanedOffices); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +const fetchResources = () => { + return new Promise((resolve, reject) => { + API.get('/resources') + .then((result) => { + const resources = result.data || []; + const cleanedResources = []; + resources.forEach(resource => { + cleanedResources.push({ + resourceId: resource['_id'], + resourceName: resource.name, + officeId: resource.office, + }); + }); + resolve(cleanedResources); + }) + .catch((error) => { + reject(error); + }); + }); +}; + +const getMappingsFromDatabase = () => { + return db.officeResourceMapping.findAll(); +}; + +const saveNewMappingToDatabase = (mapping) => { + return db.officeResourceMapping.findOrCreate({where: {...mapping}, defaults: {...mapping}}); +}; + +module.exports = { + getMappingsFromDatabase, + fetchOffices, + fetchResources, + saveNewMappingToDatabase, +};