From 79bcf91dc7a863691a8cb03c18ca01dbb0596cb2 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Thu, 30 May 2019 03:44:11 +0200 Subject: [PATCH] upload, parse and store door lock entries --- client/package.json | 1 + .../UploadDLockData/components/FileUpload.js | 54 +++++++ .../components/UploadResults.js | 61 ++++++++ client/src/scenes/UploadDLockData/index.js | 49 ++---- client/src/store/actions/doorLockActions.js | 23 ++- client/src/store/reducers/doorLockReducers.js | 5 +- client/src/utilities/api.js | 5 + client/yarn.lock | 9 +- constants/constants.js | 24 +++ controllers/doorLock.js | 42 ++++- .../20190529103954-create-door-lock-events.js | 33 ++++ models/DoorLockEvent.js | 20 +++ package-lock.json | 147 ++++++++++++++++-- package.json | 2 + routes/index.js | 2 +- services/doorLock.js | 108 +++++++++++++ 16 files changed, 528 insertions(+), 57 deletions(-) create mode 100644 client/src/scenes/UploadDLockData/components/FileUpload.js create mode 100644 client/src/scenes/UploadDLockData/components/UploadResults.js create mode 100644 client/src/utilities/api.js create mode 100644 constants/constants.js create mode 100644 migrations/20190529103954-create-door-lock-events.js create mode 100644 models/DoorLockEvent.js create mode 100644 services/doorLock.js diff --git a/client/package.json b/client/package.json index a44f884..c87178e 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^0.18.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.0.3", diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js new file mode 100644 index 0000000..d0561ff --- /dev/null +++ b/client/src/scenes/UploadDLockData/components/FileUpload.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import {Form} from "semantic-ui-react"; + +import { uploadDoorLockData } from "../../../store/actions"; + +class FileUpload extends Component { + constructor(props) { + super(props); + + this.state = { + file: null, + }; + + this.onFileChange = this.onFileChange.bind(this); + this.onUploadClick = this.onUploadClick.bind(this); + } + + onFileChange(event) { + const file = event.target.files[0]; + this.setState({file}); + }; + + onUploadClick() { + const { uploadDoorLockData } = this.props; + const { file } = this.state; + + if (file) { + uploadDoorLockData(file); + } + }; + + render() { + return ( +
+ + Upload +
+ ); + } +} + +const mapDispatchToProps = (dispatch) => ({ + uploadDoorLockData: (doorLockDataFile) => uploadDoorLockData(dispatch, doorLockDataFile) +}); + +export default connect(null, mapDispatchToProps)(FileUpload); diff --git a/client/src/scenes/UploadDLockData/components/UploadResults.js b/client/src/scenes/UploadDLockData/components/UploadResults.js new file mode 100644 index 0000000..cc688a5 --- /dev/null +++ b/client/src/scenes/UploadDLockData/components/UploadResults.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Loader, Message } from 'semantic-ui-react'; + +class UploadResults extends Component { + render(){ + const {pending, result, error} = this.props; + + const parserErrors = result && result.parserErrors && result.parserErrors.length > 0 ? result.parserErrors : null; + const parsedDataCount = result && result.parsedData && result.parsedData.length > 0 ? result.parsedData.length : null; + + return ( +
+ { + pending && + } +
+ { + error && + + Upload failed +

There was error uploading file

+
+ } +
+ { + !error && parsedDataCount && + + Upload complete +

{parsedDataCount} entries successfully inserted

+ {parserErrors &&

Some entries could not be parsed. Details are shown below

} +
+ } +
+ { + !error && parserErrors && parserErrors.map((parserError, index) => { + return ( +
+ + {parserError.error} +

{JSON.stringify(parserError.details)}

+

File : {parserError.file}

+
+
+
+ ); + }) + } +
+ ) + } +} + +const mapStateToProps = (state) => ({ + pending: state.doorLockData.pending, + result: state.doorLockData.result, + error: state.doorLockData.error, + +}); + +export default connect(mapStateToProps)(UploadResults); diff --git a/client/src/scenes/UploadDLockData/index.js b/client/src/scenes/UploadDLockData/index.js index b75da54..b2e4e52 100644 --- a/client/src/scenes/UploadDLockData/index.js +++ b/client/src/scenes/UploadDLockData/index.js @@ -1,39 +1,22 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - +import React from 'react'; import { Container, Form } from "semantic-ui-react"; import MainMenu from '../../components/MainMenu'; -import { uploadDoorLockData } from "../../store/actions"; +import FileUpload from './components/FileUpload'; +import UploadResults from './components/UploadResults'; -class UploadDLockData extends Component { - - render () { - return ( - - -

DLock Data

-
-
- - Upload - -
- ); - } +function UploadDLockData() { + return ( + + +

DLock Data

+
+
+ + + +
+ ); } -const mapStateToProps = (state) => ({ - -}); - -const mapDispatchToProps = (dispatch) => ({ - uploadDoorLockData: () => uploadDoorLockData(dispatch), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(UploadDLockData); +export default UploadDLockData; diff --git a/client/src/store/actions/doorLockActions.js b/client/src/store/actions/doorLockActions.js index 9aa82ea..00273f4 100644 --- a/client/src/store/actions/doorLockActions.js +++ b/client/src/store/actions/doorLockActions.js @@ -4,14 +4,21 @@ import { UPLOAD_DOOR_LOCK_DATA_FAILED } from "../constants"; -export const uploadDoorLockData = (dispatch) => { +import API from '../../utilities/api'; + +export const uploadDoorLockData = (dispatch, doorLockDataFile) => { + const formData = new FormData(); + formData.append('doorLockDataFile', doorLockDataFile); + const additionalConfig = { + headers: {'content-type': 'multipart/form-data'} + }; + dispatch({type: UPLOAD_DOOR_LOCK_DATA_PENDING}); - fetch('/api/doorLockData') - .then(response => response.json()) - .then(data => { - dispatch({type: UPLOAD_DOOR_LOCK_DATA_SUCCESS, payload: data}) - }) - .catch(err => { - dispatch({type: UPLOAD_DOOR_LOCK_DATA_FAILED, payload: err}) + API.post('doorLock/upload', formData, additionalConfig) + .then(response => { + dispatch({type: UPLOAD_DOOR_LOCK_DATA_SUCCESS, payload: response.data}) }) + .catch(error => { + dispatch({type: UPLOAD_DOOR_LOCK_DATA_FAILED, payload: error.response}) + }); }; diff --git a/client/src/store/reducers/doorLockReducers.js b/client/src/store/reducers/doorLockReducers.js index 759e4fd..58cc7d0 100644 --- a/client/src/store/reducers/doorLockReducers.js +++ b/client/src/store/reducers/doorLockReducers.js @@ -7,7 +7,7 @@ import { const initialState = { pending: false, result: {}, - error: '', + error: null, }; export const doorLockData = (state, action) => { @@ -18,15 +18,18 @@ export const doorLockData = (state, action) => { case UPLOAD_DOOR_LOCK_DATA_PENDING: return Object.assign({}, state, { pending: true, + error: null, }); case UPLOAD_DOOR_LOCK_DATA_SUCCESS: return Object.assign({}, state, { pending: false, result: action.payload, + error: null, }); case UPLOAD_DOOR_LOCK_DATA_FAILED: return Object.assign({}, state, { pending: false, + result: {}, error: action.payload, }); default: diff --git a/client/src/utilities/api.js b/client/src/utilities/api.js new file mode 100644 index 0000000..264adff --- /dev/null +++ b/client/src/utilities/api.js @@ -0,0 +1,5 @@ +import axios from 'axios'; + +export default axios.create({ + baseURL: '/api/' +}); diff --git a/client/yarn.lock b/client/yarn.lock index 2f7cadb..8413b54 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1597,6 +1597,13 @@ aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" +axios@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -3547,7 +3554,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.3.0: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" dependencies: diff --git a/constants/constants.js b/constants/constants.js new file mode 100644 index 0000000..4de18b5 --- /dev/null +++ b/constants/constants.js @@ -0,0 +1,24 @@ +const USER_ENTRY_EVENT = 'User Entry'; +const ENABLE_PASSAGE_MODE = 'Enable Passage Mode by Group 2'; +const DISABLE_PASSAGE_MODE = 'Disable Passage Mode by Group 2'; + +const USER_LOCKED_DOOR = 'locked'; +const USER_UNLOCKED_DOOR = 'unlocked'; + +const VALID_CSV_HEADERS = ['Date', 'Time', 'User No', 'Name', 'Event']; + +const csvParserErrors = { + UNKNOWN_COLUMN: 'Unknown column', + INVALID_ENTRY_EXPECTED_USER: 'Invalid entry type. Expected user entry type', + INVALID_ENTRY_EXPECTED_PASSAGE_MODE: 'Invalid entry type. Expected enable/disable passage mode', +}; + +module.exports = { + VALID_CSV_HEADERS, + USER_ENTRY_EVENT, + ENABLE_PASSAGE_MODE, + DISABLE_PASSAGE_MODE, + USER_LOCKED_DOOR, + USER_UNLOCKED_DOOR, + csvParserErrors, +}; diff --git a/controllers/doorLock.js b/controllers/doorLock.js index 8376c64..7925263 100644 --- a/controllers/doorLock.js +++ b/controllers/doorLock.js @@ -1,7 +1,47 @@ 'use strict'; +const { parseDoorLockDataFile, writeDoorLockEvent } = require("../services/doorLock"); + +const IncomingForm = require('formidable').IncomingForm; + + const uploadDoorLockData = (req, res) => { - res.json({status: 'ok'}); + const form = new IncomingForm(); + const parsingResults = []; + + form.on('file', (field, file) => { + if (file && file.type === 'text/csv') { + parsingResults.push(parseDoorLockDataFile(file)); + } + }); + + form.on('end', () => { + Promise.all(parsingResults) + .then((parserResults) => { + const parsedData = []; + const parserErrors = []; + + parserResults.forEach((parserResult) => { + parsedData.push(...parserResult.parsedData); + parserErrors.push(...parserResult.errors); + }); + + res.json({ + parsedData, + parserErrors, + }); + + parsedData.forEach((entry) => { + writeDoorLockEvent(entry); + }); + }) + .catch((error) => { + console.log(error); + res.json({result: 'error'}); + }); + }); + + form.parse(req); }; module.exports = { diff --git a/migrations/20190529103954-create-door-lock-events.js b/migrations/20190529103954-create-door-lock-events.js new file mode 100644 index 0000000..93990bd --- /dev/null +++ b/migrations/20190529103954-create-door-lock-events.js @@ -0,0 +1,33 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('doorLockEvents', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + memberName: Sequelize.STRING, + memberNumber: Sequelize.INTEGER, + event: { + type: Sequelize.ENUM, + values: ['locked', 'unlocked'] + }, + date: Sequelize.DATEONLY, + time: Sequelize.TIME, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('doorLockEvents'); + } +}; diff --git a/models/DoorLockEvent.js b/models/DoorLockEvent.js new file mode 100644 index 0000000..f159dff --- /dev/null +++ b/models/DoorLockEvent.js @@ -0,0 +1,20 @@ +'use strict'; + +const { USER_LOCKED_DOOR, USER_UNLOCKED_DOOR } = require('../constants/constants'); + +module.exports = (sequelize, DataTypes) => { + const doorLockEvent = sequelize.define('doorLockEvent', { + memberName: DataTypes.STRING, + memberNumber: DataTypes.INTEGER, + event: { + type: DataTypes.ENUM, + values: [USER_LOCKED_DOOR, USER_UNLOCKED_DOOR] + }, + date: DataTypes.DATEONLY, + time: DataTypes.TIME, + }, {}); + doorLockEvent.associate = function(models) { + // associations can be defined here + }; + return doorLockEvent; +}; diff --git a/package-lock.json b/package-lock.json index f454197..c2b51d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -292,6 +292,30 @@ } } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -542,8 +566,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-error-class": { "version": "3.0.2", @@ -571,6 +594,56 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "csv-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-2.3.0.tgz", + "integrity": "sha512-yfYRZ9P9LNKuRK0lEFf40Be4HcFxe1XgxWL/QmlkakSE8SHcWbOGFZA/u7YpfGX/hVbRUdbnO5xO8XuYxrcBtA==", + "requires": { + "buffer-alloc": "^1.1.0", + "buffer-from": "^1.0.0", + "execa": "^1.0.0", + "generate-function": "^1.0.1", + "generate-object-property": "^1.0.0", + "minimist": "^1.2.0", + "ndjson": "^1.4.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -1051,6 +1124,11 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -1633,6 +1711,19 @@ } } }, + "generate-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-1.1.0.tgz", + "integrity": "sha1-VMIbCAGSsW2Yd3ecW7gWZudyNl8=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "^1.0.0" + } + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -2006,6 +2097,11 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -2032,8 +2128,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2069,6 +2164,11 @@ } } }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2264,8 +2364,7 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { "version": "1.3.1", @@ -2347,6 +2446,17 @@ "to-regex": "^3.0.1" } }, + "ndjson": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", + "integrity": "sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg=", + "requires": { + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.0", + "split2": "^2.1.0", + "through2": "^2.0.3" + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -2750,8 +2860,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "proto-list": { "version": "1.2.4", @@ -2824,7 +2933,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3287,6 +3395,14 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -3326,7 +3442,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3373,6 +3488,15 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -3641,8 +3765,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", diff --git a/package.json b/package.json index 2aeaae4..7e736b1 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "node": "11.12.x" }, "dependencies": { + "csv-parser": "^2.3.0", "dotenv": "^8.0.0", "express": "^4.17.0", "express-basic-auth": "^1.2.0", + "formidable": "^1.2.1", "pg": "^7.11.0", "sequelize": "^5.8.6", "sequelize-cli": "^5.4.0" diff --git a/routes/index.js b/routes/index.js index f5a4487..f5b4e2c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,6 +7,6 @@ const express = require('express'); const router = express.Router(); router.get('/', apiStatusCheck); -router.get('/doorLockData', uploadDoorLockData); +router.post('/doorLock/upload', uploadDoorLockData); module.exports = router; diff --git a/services/doorLock.js b/services/doorLock.js new file mode 100644 index 0000000..c2552b7 --- /dev/null +++ b/services/doorLock.js @@ -0,0 +1,108 @@ +'use strict'; + +const db = require('../models/index'); +const fs = require('fs'); +const csv = require('csv-parser'); + +const { + USER_ENTRY_EVENT, + ENABLE_PASSAGE_MODE, + DISABLE_PASSAGE_MODE, + USER_UNLOCKED_DOOR, + USER_LOCKED_DOOR, + VALID_CSV_HEADERS, + csvParserErrors, +} = require('../constants/constants'); + + +const parseDoorLockDataFile = (file) => { + return new Promise ((resolve, reject) => { + const results = []; + const errors = []; + let isValidFile = true; + + fs.createReadStream(file.path) + .pipe(csv({ + mapHeaders: ({ header, index }) => header.trim().toLowerCase(), + mapValues: ({ header, index, value }) => value.trim() + })) + .on('headers', (headers) => { + headers.forEach((header) => { + if (!VALID_CSV_HEADERS.includes(header.trim())){ + isValidFile = false; + console.log('INVALID HEADER'); + errors.push({ + error: csvParserErrors.UNKNOWN_COLUMN, + details: header, + 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 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)){ + if (secondEntry && (secondEntry.event === ENABLE_PASSAGE_MODE || secondEntry.event === DISABLE_PASSAGE_MODE)){ + const event = (secondEntry.event === ENABLE_PASSAGE_MODE) ? USER_UNLOCKED_DOOR : USER_LOCKED_DOOR; + const entryData = { + memberName: firstEntry.name, + memberNumber: firstEntry['user no'], + date: firstEntry.date, + time: firstEntry.time, + event, + }; + parsedData.push(entryData); + i+=2; + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_PASSAGE_MODE, + details: secondEntry || 'Last row in file', + file: file.name, + }); + i+=1; + } + } else { + errors.push({ + error: csvParserErrors.INVALID_ENTRY_EXPECTED_USER, + details: firstEntry, + file: file.name, + }); + i+=1; + } + } + resolve({ + parsedData, + errors + }); + }); + }); +}; + +const writeDoorLockEvent = (entry) => { + db.doorLockEvent.findOrCreate({where: {...entry}, defaults: {...entry}}) + .then() + .catch(); +}; + +module.exports = { + parseDoorLockDataFile, + writeDoorLockEvent, +};