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,
+};