diff --git a/.eslintrc.json b/.eslintrc.json index 7a3bed9..9d8ed29 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,12 +6,10 @@ "parserOptions": { "sourceType": "script" }, - "plugins": ["prettier"], "rules": { "no-shadow": ["error", { "allow": ["err"] }], "handle-callback-err": "error", "prefer-arrow-callback": "error", - "curly": ["error", "multi", "consistent"], - "prettier/prettier": "error" + "curly": ["error", "multi", "consistent"] } } diff --git a/client/.eslintrc.json b/client/.eslintrc.json index dcd577e..6881637 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -1,6 +1,9 @@ { "env": { - "browser": true, - "commonjs": true + "browser": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 8 } } diff --git a/client/app.js b/client/app.js index 5f20515..2781f38 100644 --- a/client/app.js +++ b/client/app.js @@ -1,5 +1,43 @@ "use strict"; -const m = require("mithril"); +import m from "mithril"; -m.render(document.body, "hello world"); +import layout from "./layout"; +import * as overviewPage from "./overview-page"; +import * as devicesPage from "./devices-page"; +import * as errorPage from "./error-page"; + +let state; + +function pagify(pageName, page) { + const component = { + render: () => { + if (state && state.error) return m(errorPage.component, state); + else return m(layout, { page: pageName }, m(page.component, state)); + } + }; + + if (page.init) + component.onmatch = args => { + return new Promise(resolve => { + page + .init(args) + .then(st => { + if (!st) return m.reoute.set("/"); + state = st; + resolve(); + }) + .catch(err => { + state = { error: err.message }; + resolve(); + }); + }); + }; + + return component; +} + +m.route(document.body, "/overview", { + "/overview": pagify("overview", overviewPage), + "/devices": pagify("devices", devicesPage) +}); diff --git a/client/config.js b/client/config.js new file mode 100644 index 0000000..20f5748 --- /dev/null +++ b/client/config.js @@ -0,0 +1,24 @@ +"use strict"; + +export default { + pageSize: 10, + index: [ + { + label: "Serial number", + parameter: "DeviceID.SerialNumber" + }, + { + label: "Product class", + parameter: "DeviceID.ProductClass" + }, + { + label: "Software version", + parameter: "InternetGatewayDevice.DeviceInfo.SoftwareVersion" + }, + { + label: "MAC", + parameter: + "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress" + } + ] +}; diff --git a/client/css/app.css b/client/css/app.css new file mode 100644 index 0000000..5454683 --- /dev/null +++ b/client/css/app.css @@ -0,0 +1,202 @@ +@import "normalize.css"; + +:root { + --base-font-size: 13px; + --base-line-height: 16px; + + --color1: #ebe0bf; + --color2: #b9b098; + --color3: #f2ebd5; + --color4: #046380; + --color5: #002f2f; + + --fade: 0.2s; +} + +body, +input, +button, +select, +option, +textarea { + font-family: sans-serif; + color: var(--color5); +} + +a { + color: var(--color4); + cursor: pointer; + transition: background-color var(--fade), color var(--fade); + &:visited { + color: var(--color4); + } + &:hover { + color: white; + background-color: var(--color4); + } +} + +strong, +b { + font-weight: bold; +} + +i { + font-style: italic; +} + +#header { + background-color: #f2eddf; + border-bottom: 1px solid var(--color1); + overflow: visible; + position: relative; + + & > nav { + display: inline-block; + overflow: visible; + position: absolute; + bottom: -1px; + + & > ul { + font-weight: bold; + display: inline-block; + margin: 0 10px; + padding: 0; + + & > li { + margin: 0 2px; + background-color: var(--color1); + display: inline-block; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + transition: background-color var(--fade); + &.active, + &:hover { + background-color: white; + } + + & > a, + & > a:visited, + & > a:hover { + display: inline-block; + min-width: 140px; + color: var(--color4); + text-decoration: none; + background: none; + padding: 8px; + } + } + } + } + & > .logo { + margin: 10px; + display: inline; + height: 56px; + } +} + +#content { + margin: 10px; + min-height: 250px; +} + +table.table { + border-spacing: 0; + text-align: left; + + & th { + color: var(--color4); + font-weight: bold; + border-bottom: 2px solid var(--color4); + margin: 0; + padding: 0.2em 0.5em; + } + + & td { + margin: 0; + padding: 0.2em 0.5em; + border-bottom: 1px dotted var(--color1); + & .action a { + visibility: hidden; + } + } + + & tbody tr:hover td { + & .action { + visibility: visible; + } + + transition: background-color var(--fade); + background-color: var(--color3); + } + + & tfoot td > a { + margin-left: 1em; + } +} + +h1 { + color: var(--color4); + font-size: 130%; + font-weight: bold; + margin-top: 1em; + margin-bottom: 0.5em; +} + +h2 { + color: var(--color5); + font-weight: bold; + margin-top: 1em; + margin-bottom: 0.5em; +} + +h3 { + color: var(--color5); + margin-top: 1em; + margin-bottom: 0.5em; +} + +/* Remove button padding in FF */ +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input[type="button"], +button { + display: inline-block; + min-width: 6em; + position: relative; + top: 0px; + vertical-align: baseline; + margin: 0 0.1em; + outline: none; + cursor: pointer; + text-align: center; + text-decoration: none; + padding: 0.1em 0.6em; + border-top-left-radius: 1em; + border-bottom-right-radius: 1em; + border-top-right-radius: 0.2em; + border-bottom-left-radius: 0.2em; + + color: var(--color4); + transition: background-color var(--fade), color var(--fade); + + &:link, + &:visited { + color: var(--color4); + background-color: var(--color1); + } + + &:hover, + &:active { + color: var(--color1); + background-color: var(--color4); + } + + &:disabled { + background-color: var(--color1); + color: var(--color2); + } +} diff --git a/client/css/normalize.css b/client/css/normalize.css new file mode 100644 index 0000000..04beed5 --- /dev/null +++ b/client/css/normalize.css @@ -0,0 +1,450 @@ +/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { + /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * 1. Remove the bottom border in Chrome 57- and Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], +/* 1 */ [type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, +/* 1 */ menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/client/devices-page.js b/client/devices-page.js new file mode 100644 index 0000000..ad6223d --- /dev/null +++ b/client/devices-page.js @@ -0,0 +1,115 @@ +"use strict"; + +import m from "mithril"; + +import config from "./config"; + +function load(filter, limit) { + return new Promise((resolve, reject) => { + m + .request({ + url: "/api/devices/", + data: { filter: JSON.stringify(filter), limit: limit } + }) + .then(res => { + resolve(res); + }) + .catch(err => { + err.message = err.message || "Unknown error"; + reject(err); + }); + }); +} + +function count(filter, limit) { + return new Promise((resolve, reject) => { + function extract(xhr) { + return +xhr.getResponseHeader("x-total-count"); + } + m + .request({ + method: "HEAD", + url: "/api/devices/", + data: { filter: JSON.stringify(filter), limit: limit }, + extract: extract + }) + .then(res => { + resolve(res); + }) + .catch(err => { + err.message = err.message || "Unknown error"; + reject(err); + }); + }); +} + +function init(args) { + return new Promise((resolve, reject) => { + Promise.all([count(args.filter), load(args.filter, config.pageSize)]) + .then(res => { + resolve({ filter: args.filter, total: res[0], devices: res[1] }); + }) + .catch(reject); + }); +} + +function renderTable(devices, parameters, total, showMoreCallback) { + let labels = []; + for (let param of parameters) labels.push(m("th", param.label)); + + let rows = []; + for (let device of devices) + rows.push( + m( + "tr", + parameters.map(p => { + if (!device[p.parameter] || !device[p.parameter].value) + return m("td.na"); + if (!device[p.parameter].value[0] === "") return m("td.blank"); + return m("td", device[p.parameter].value[0]); + }) + ) + ); + + let footerElements = [`${devices.length}/${total}`]; + + if (devices.length < total) + footerElements.push(m("a", { onclick: showMoreCallback }, "Show more")); + + let tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + return m("table.table", [ + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ]); +} + +const component = { + view: vnode => { + document.title = "Devices - GenieACS"; + function showMore() { + const lastDevice = vnode.attrs.devices[vnode.attrs.devices.length - 1]; + let f = Object.assign({}, vnode.attrs.filter, { + "DeviceID.ID>": lastDevice["DeviceID.ID"].value[0] + }); + load(f, config.pageSize).then(res => { + vnode.attrs.devices = vnode.attrs.devices.concat(res); + }); + } + return [ + m("h1", "Listing devices"), + renderTable( + vnode.attrs.devices, + config.index, + vnode.attrs.total, + showMore + ) + ]; + } +}; + +export { init, component }; diff --git a/client/error-page.js b/client/error-page.js new file mode 100644 index 0000000..4398d87 --- /dev/null +++ b/client/error-page.js @@ -0,0 +1,10 @@ +"use strict"; + +const component = { + view: function(vnode) { + document.title = "Error! - GenieACS"; + return vnode.attrs.error; + } +}; + +export { component }; diff --git a/client/layout.js b/client/layout.js new file mode 100644 index 0000000..bacab47 --- /dev/null +++ b/client/layout.js @@ -0,0 +1,19 @@ +"use strict"; + +import m from "mithril"; + +import menu from "./menu"; + +const layout = { + view: vnode => { + return [ + m("#header", [ + m("img.logo", { src: "logo.svg" }), + m(menu, { page: vnode.attrs.page }) + ]), + m("#content", [vnode.children]) + ]; + } +}; + +export default layout; diff --git a/client/menu.js b/client/menu.js new file mode 100644 index 0000000..d7c32bb --- /dev/null +++ b/client/menu.js @@ -0,0 +1,27 @@ +"use strict"; + +import m from "mithril"; + +const menu = { + view: vnode => { + const active = { [vnode.attrs.page]: "active" }; + + return m( + "nav", + m("ul", [ + m( + "li", + { class: active["overview"] }, + m("a", { href: "#!/overview" }, "Overview") + ), + m( + "li", + { class: active["devices"] }, + m("a", { href: "#!/devices" }, "Devices") + ) + ]) + ); + } +}; + +export default menu; diff --git a/client/overview-page.js b/client/overview-page.js new file mode 100644 index 0000000..35c091f --- /dev/null +++ b/client/overview-page.js @@ -0,0 +1,22 @@ +"use strict"; + +import m from "mithril"; + +const init = function() { + return new Promise(resolve => { + resolve({ message: "Hello world!" }); + }); +}; + +const component = { + view: vnode => { + document.title = "Overview - GenieACS"; + return m( + "a", + { href: "#!/devices/202BC1-BM632w-000000" }, + vnode.attrs.message + ); + } +}; + +export { init, component }; diff --git a/package.json b/package.json index 75fa1d6..e7cc313 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint --fix server/*.js client/*.js", - "build": "webpack", - "start": "npm run lint;npm run build;node server/server" + "lint": "eslint --fix server/*.js client/*.js;prettier --write server/*.js client/*.js client/css/*.css", + "build": "npm run lint;webpack;postcss -c postcss.config.js client/css/app.css -o public/app.css", + "start": "node server/server" }, "author": "Zaid Abdulla ", "dependencies": { @@ -17,8 +17,11 @@ "mithril": "^1.1.5" }, "devDependencies": { + "cssnano": "^3.10.0", "eslint": "^4.12.1", - "eslint-plugin-prettier": "^2.3.1", + "postcss-cli": "^4.1.1", + "postcss-cssnext": "^3.0.2", + "postcss-import": "^11.0.0", "prettier": "^1.8.2", "webpack": "^3.9.1" } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..91a9f0d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require("postcss-import"), + require("postcss-cssnext")({warnForDuplicates: false}), + require("cssnano") + ] +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..1f16570 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/index.html b/public/index.html index 772daa5..cc4fe7c 100644 --- a/public/index.html +++ b/public/index.html @@ -1,5 +1,8 @@ + GenieACS + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..2352be8 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/api-functions.js b/server/api-functions.js index 706a63b..131dfe1 100644 --- a/server/api-functions.js +++ b/server/api-functions.js @@ -6,6 +6,7 @@ const url = require("url"); const querystring = require("querystring"); const config = require("./config"); +const device = require("./device"); const commaAscii = ",".charCodeAt(0); const newLineAscii = "\n".charCodeAt(0); @@ -19,6 +20,7 @@ function filtersToQuery(filters) { for (let [k, v] of Object.entries(filter)) { let [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2); if (!op || op === "=") q[param] = v; + else if (op === ">") q[param] = { $gt: v }; else throw new Error(`Operator "${op}" not recognized`); } queries.push(q); @@ -28,22 +30,63 @@ function filtersToQuery(filters) { else return { $or: queries }; } +function count(resource, filters, limit) { + return new Promise((resolve, reject) => { + let qs = {}; + if (filters) { + let q = filtersToQuery(filters); + if (resource === "devices") q = device.transposeQuery(q); + qs.query = JSON.stringify(q); + } + + if (limit) qs.limit = limit; + + let options = url.parse( + `${config.GENIEACS_NBI}${resource}?${querystring.stringify(qs)}` + ); + + options.method = "HEAD"; + + let _http = options.protocol === "https:" ? https : http; + + _http + .request(options, res => { + if (res.statusCode !== 200) { + reject(new Error(`Unexpected status code ${res.statusCode}`)); + res.resume(); + return; + } + resolve(+res.headers["total"]); + }) + .end(); + }); +} + function query(resource, filters, limit, callback) { return new Promise((resolve, reject) => { let ret; if (!callback) ret = []; - let q = {}; - if (filters) q.query = JSON.stringify(filtersToQuery(filters)); + let qs = {}; + if (filters) { + let q = filtersToQuery(filters); + if (resource === "devices") q = device.transposeQuery(q); + qs.query = JSON.stringify(q); + } - if (limit) q.limit = limit; + if (limit) qs.limit = limit; let options = url.parse( - `${config.GENIEACS_NBI}${resource}?${querystring.stringify(q)}` + `${config.GENIEACS_NBI}${resource}?${querystring.stringify(qs)}` ); let _http = options.protocol === "https:" ? https : http; _http.get(options, res => { + if (res.statusCode !== 200) { + reject(new Error(`Unexpected status code ${res.statusCode}`)); + res.resume(); + return; + } let chunks = []; let bytes = 0; res.on("data", chunk => { @@ -69,6 +112,7 @@ function query(resource, filters, limit, callback) { if (buf.length > 1) { let obj = JSON.parse(buf); + if (resource === "devices") obj = device.transpose(obj); if (ret) ret.push(obj); else callback(obj); } @@ -89,6 +133,8 @@ function query(resource, filters, limit, callback) { } if (buf.length > 1) { let obj = JSON.parse(buf); + if (resource === "devices") obj = device.transpose(obj); + if (ret) ret.push(obj); else callback(obj); } @@ -104,3 +150,4 @@ function query(resource, filters, limit, callback) { } exports.query = query; +exports.count = count; diff --git a/server/api.js b/server/api.js index cd09895..d7e3381 100644 --- a/server/api.js +++ b/server/api.js @@ -16,35 +16,35 @@ const resources = { for (let [resource, update] of Object.entries(resources)) { router.head(`/${resource}`, async ctx => { - let filters, limit; - if (ctx.request.query.query) - if (Array.isArray(ctx.request.query.query)) - filters = ctx.request.query.query.map(q => JSON.parse(q)); + let filter, limit; + if (ctx.request.query.filter) + if (Array.isArray(ctx.request.query.filter)) + filter = ctx.request.query.query.map(q => JSON.parse(q)); else { - filters = JSON.parse(ctx.request.query.query); - if (!Array.isArray(filters)) filters = [filters]; + filter = JSON.parse(ctx.request.query.filter); + if (!Array.isArray(filter)) filter = [filter]; } if (ctx.request.query.limit) limit = +ctx.request.query.limit; - let res = await apiFunctions.query(resource, filters, limit); - ctx.set("X-Total-Count", res.length); + let count = await apiFunctions.count(resource, filter, limit); + ctx.set("X-Total-Count", count); ctx.body = ""; }); router.get(`/${resource}`, async ctx => { - let filters, limit; - if (ctx.request.query.query) - if (Array.isArray(ctx.request.query.query)) - filters = ctx.request.query.query.map(q => JSON.parse(q)); + let filter, limit; + if (ctx.request.query.filter) + if (Array.isArray(ctx.request.query.filter)) + filter = ctx.request.query.query.map(q => JSON.parse(q)); else { - filters = JSON.parse(ctx.request.query.query); - if (!Array.isArray(filters)) filters = [filters]; + filter = JSON.parse(ctx.request.query.filter); + if (!Array.isArray(filter)) filter = [filter]; } if (ctx.request.query.limit) limit = +ctx.request.query.limit; - ctx.body = await apiFunctions.query(resource, filters, limit); + ctx.body = await apiFunctions.query(resource, filter, limit); }); router.head(`/${resource}/:id`, async (ctx, next) => { diff --git a/server/device.js b/server/device.js new file mode 100644 index 0000000..aefea8f --- /dev/null +++ b/server/device.js @@ -0,0 +1,152 @@ +"use strict"; + +function recursive(input, root, output, timestamp) { + for (let [name, tree] of Object.entries(input)) { + if (root.length === 0) + if (name === "_lastInform") { + output["Events.Inform"] = { + value: [timestamp, "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_registered") { + output["Events.Registered"] = { + value: [timestamp, "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_lastBoot") { + output["Events.1_BOOT"] = { + value: [timestamp, "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_lastBootstrap") { + output["Events.0_BOOTSTRAP"] = { + value: [timestamp, "xsd:dateTime"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_id") { + output["DeviceID.ID"] = { + value: [tree, "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_deviceId") { + output["DeviceID.Manufacturer"] = { + value: [tree["_Manufacturer"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.OUI"] = { + value: [tree["_OUI"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.ProductClass"] = { + value: [tree["_ProductClass"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + output["DeviceID.SerialNumber"] = { + value: [tree["_SerialNumber"], "xsd:string"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } else if (name === "_tags") { + output["Tags"] = { + writable: true, + writableTimestamp: timestamp, + object: true, + objectTimestamp: timestamp + }; + + for (let t of tree) + output[`Tags.${t}`] = { + value: [true, "xsd:boolean"], + valueTimestamp: timestamp, + writable: false, + writableTimestamp: timestamp, + object: false, + objectTimestamp: timestamp + }; + } + + if (name.startsWith("_")) continue; + + let childrenTimestamp = timestamp; + + if (root.length === 0) childrenTimestamp = input["_timestamp"] || 1; + else if (input["_timestamp"] > timestamp) + childrenTimestamp = input["_timestamp"]; + + const attrs = {}; + if (tree["_value"]) { + attrs["value"] = [tree["_value"], tree["_type"]]; + attrs["valueTimestamp"] = childrenTimestamp; + attrs["object"] = false; + attrs["objectTimestamp"] = childrenTimestamp; + } else if (tree["_object"]) { + attrs["object"] = tree["_object"]; + attrs["objectTimestamp"] = childrenTimestamp; + } + + if (tree["_writable"]) { + attrs["writable"] = tree["_writable"]; + attrs["writableTimestamp"] = childrenTimestamp; + } + + let r = root.concat(name); + output[r.join(".")] = attrs; + + if (attrs["object"]) recursive(tree, r, output, childrenTimestamp); + } +} + +function transpose(device) { + const newDevice = {}; + const timestamp = new Date(device["_lastInform"] || 1).getTime(); + recursive(device, [], newDevice, timestamp); + return newDevice; +} + +function transposeQuery(query) { + const newQuery = {}; + + for (let [key, val] of Object.entries(query)) + if (key.startsWith("$")) newQuery[key] = transposeQuery(val); + else if (key === "DeviceID.ID") newQuery["_id"] = val; + + return newQuery; +} + +exports.transpose = transpose; +exports.transposeQuery = transposeQuery;