Compare commits

..

3 Commits

9 changed files with 552 additions and 315 deletions

61
package-lock.json generated
View File

@ -16,6 +16,7 @@
"express": "^4.21.2", "express": "^4.21.2",
"express-fileupload": "^1.5.1", "express-fileupload": "^1.5.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"passport": "^0.7.0", "passport": "^0.7.0",
@ -28,6 +29,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/express-session": "^1.18.1", "@types/express-session": "^1.18.1",
"@types/joi": "^17.2.2",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.14",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/passport": "^1.0.17", "@types/passport": "^1.0.17",
@ -619,6 +621,21 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1032,6 +1049,27 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -1154,6 +1192,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/joi": {
"version": "17.2.2",
"resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz",
"integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==",
"dev": true,
"license": "MIT",
"dependencies": {
"joi": "*"
}
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -3585,6 +3633,19 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jquery": { "node_modules/jquery": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",

View File

@ -24,6 +24,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/express-session": "^1.18.1", "@types/express-session": "^1.18.1",
"@types/joi": "^17.2.2",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.14",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/passport": "^1.0.17", "@types/passport": "^1.0.17",
@ -45,6 +46,7 @@
"express": "^4.21.2", "express": "^4.21.2",
"express-fileupload": "^1.5.1", "express-fileupload": "^1.5.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"joi": "^17.13.3",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"passport": "^0.7.0", "passport": "^0.7.0",

View File

@ -0,0 +1,22 @@
/**
* A function to create a sortBy compatible object from a string
*
* @export
* @param {string} SortField
* @param {string} Order
* @returns {object}
*/
export function parseDynamicSortBy(SortField: string, Order: string) {
return JSON.parse(`{ "${SortField}": "${Order}" }`);
}
/**
* Function to parse a string into a number or return undefined if it is not a number
*
* @export
* @param {string || any} data
* @returns {object}
*/
export function parseIntOrUndefined(data: any) {
return isNaN(parseInt(data)) ? undefined : parseInt(data);
}

View File

@ -1,116 +1,87 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
import log from '../../../handlers/log.js'; import log from '../../../handlers/log.js';
import { parseIntOrUndefined, parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
import { schema_get } from './alertContacts_schema.js';
///api/v1/alertContacts?action=count&filter=... // MARK: GET AlertContact
// GET without args -> Get all alertContacts
/**
* A function to create a sortBy compatible object from a string
*
* @export
* @param {string} SortField
* @param {string} Order
* @returns {object}
*/
export function parseDynamicSortBy(SortField: string, Order: string) {
return JSON.parse(`{ "${SortField}": "${Order}" }`);
}
/**
* Function to parse a string into a number or return undefined if it is not a number
*
* @export
* @param {string || any} data
* @returns {object}
*/
export function parseIntOrUndefined(data: any) {
return isNaN(parseInt(data)) ? undefined : parseInt(data);
}
// GET AlertContact
async function get(req: Request, res: Response) { async function get(req: Request, res: Response) {
// Set sane defaults if undefined for sort const { error, value } = schema_get.validate(req.query);
if (req.query.sort === undefined) { if (error) {
req.query.sort = 'id'; log.api?.debug('Error:', req.query, value);
} res.status(400).json({ error: error.details[0].message });
if (req.query.order === undefined) { } else {
req.query.order = 'asc'; log.api?.debug('Success:', req.query, value);
}
// Prio 1 -> Get count (with or without filter) // Query with FullTextSearch
// Prio 2 -> Get by id const query_fts = {
// Prio 3 -> Get with filter
if ((req.query.search !== undefined && req.query.search.length > 0) || (req.query.id !== undefined && req.query.id.length > 0)) {
if (req.query.search !== undefined && req.query.search === '*') {
log.db.debug('Single * does not work with FullTextSearch');
req.query.search = '';
}
// When an ID is set, remove(disable) the search query
if (req.query.id !== undefined && req.query.id.length > 0) {
req.query.search = undefined;
}
const query = {
where: { where: {
OR: [{ id: parseIntOrUndefined(req.query.id) }, { name: { search: req.query.search } }, { phone: { search: req.query.search } }, { comment: { search: req.query.search } }] OR: [{ id: parseIntOrUndefined(value.id) }, { name: { search: value.search } }, { phone: { search: value.search } }, { comment: { search: value.search } }]
}, },
orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()) orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString())
}; };
if (req.query.count === undefined) { if (value.search !== undefined || value.id !== undefined) {
// get all entrys // with FullTextSearch
await db.alertContacts.findMany(query).then((result) => { if (!value.count) {
res.status(200).json(result); // get all entrys
}); log.api?.trace('get all entrys - with FullTextSearch');
await db.alertContacts.findMany(query_fts).then((result) => {
res.status(200).json(result);
});
} else {
// count all entrys
log.api?.trace('count all entrys - with FullTextSearch');
await db.alertContacts.count(query_fts).then((result) => {
res.status(200).json(result);
});
}
} else { } else {
// count all entrys (filtered or not) // without FullTextSearch
await db.alertContacts.count(query).then((result) => { if (!value.count) {
res.status(200).json(result); // get all entrys
}); log.api?.trace('get all entrys - without FullTextSearch');
} await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()) }).then((result) => {
} else { res.status(200).json(result);
if (req.query.count === undefined) { });
await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()) }).then((result) => { } else {
res.status(200).json(result); // count all entrys without FullTextSearch
}); log.api?.trace('count all entrys - without FullTextSearch');
} else { await db.alertContacts.count().then((result) => {
await db.alertContacts.count().then((result) => { res.status(200).json(result);
res.status(200).json(result); });
}); }
} }
} }
} }
// CREATE AlertContact // MARK: CREATE AlertContact
async function post(req: Request, res: Response) { async function post(req: Request, res: Response) {
// Check if undefined or null // Check if undefined or null
if (req.body.name != null && req.body.phone != null) { if (req.body.name != null && req.body.phone != null) {
await db.alertContacts await db.alertContacts
.create({ .create({
data: { data: {
name: req.body.name, name: req.body.name,
phone: req.body.phone, phone: req.body.phone,
comment: req.body.comment, comment: req.body.comment
}, },
select: { select: {
id: true id: true
} }
}).then((result) => { })
res.status(201).json({ status: 'CREATED', message: 'Successfully created alertContact', id: result.id }); .then((result) => {
}) res.status(201).json({ status: 'CREATED', message: 'Successfully created alertContact', id: result.id });
});
} else { } else {
res.status(400).json({ status: 'ERROR', errorcode: "VALIDATION_ERROR", message: 'One or more required fields are missing or invalid' }); res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing or invalid' });
} }
} }
// UPDATE AlertContact // MARK: UPDATE AlertContact
async function patch(req: Request, res: Response) {} async function patch(req: Request, res: Response) {}
// DELETE AlertContact // MARK: DELETE AlertContact
async function del(req: Request, res: Response) {} async function del(req: Request, res: Response) {}
export default { get, post, patch, del }; export default { get, post, patch, del };

View File

@ -0,0 +1,53 @@
import { Request, Response } from 'express';
import validator from 'joi'; // DOCS: https://joi.dev/api
import log from '../../../handlers/log.js';
const schema_get = validator.object({
sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
order: validator.string().valid('asc', 'desc').default('asc'),
search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
id: validator.number().positive().precision(0),
count: validator.boolean()
}).nand('id', 'search'); // Allow id or search. not both.
const schema_post = validator.object({
name: validator.string().min(1).max(32).required(),
phone: validator.string().pattern(new RegExp('^\\+(\\d{1,3})\\s*(?:\\(\\s*(\\d{2,5})\\s*\\)|\\s*(\\d{2,5})\\s*)\\s*(\\d{5,15})$')).required(),
comment: validator.string().max(64),
})
const schema_patch = validator.object({
sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
order: validator.string().valid('asc', 'desc').default('asc'),
search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
id: validator.number().positive().precision(0),
count: validator.boolean()
}).nand('id', 'search'); // Allow id or search. not both.
const schema_del = validator.object({
sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'),
order: validator.string().valid('asc', 'desc').default('asc'),
search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes..
id: validator.number().positive().precision(0),
count: validator.boolean()
}).nand('id', 'search'); // Allow id or search. not both.
// Describe all schemas
const schema_get_describe = schema_get.describe();
const schema_post_describe = schema_post.describe();
const schema_patch_describe = schema_patch.describe();
const schema_del_describe = schema_del.describe();
// GET route
export default async function get(req: Request, res: Response) {
res.status(200).json({
GET: schema_get_describe,
POST: schema_post_describe,
PATCH: schema_patch_describe,
DELETE: schema_del_describe
});
}
export { schema_get, schema_post, schema_patch, schema_del }

View File

@ -3,14 +3,11 @@ import passport from 'passport';
// Route imports // Route imports
import testRoute from './test.js'; import testRoute from './test.js';
import alertContactsRoute from './alertContacts.js';
//import categoryRoute from './categories.js';
//import storageUnitRoute from './storageUnits.js';
//import storageLocationRoute from './storageLocations.js';
//import contactInfo from './contactInfo.js';
import versionRoute from './version.js' import versionRoute from './version.js'
//import search_routes from './search/index.js'; import alertContactsRoute from './alertContacts.js';
import alertContactsRoute_schema from './alertContacts_schema.js';
// Router base is '/api/v1' // Router base is '/api/v1'
const Router = express.Router({ strict: false }); const Router = express.Router({ strict: false });
@ -27,11 +24,7 @@ Router.use('*', function (req, res, next) {
// All api routes lowercase! Yea I know but when strict: true it matters. // All api routes lowercase! Yea I know but when strict: true it matters.
Router.route('/alertcontacts').get(alertContactsRoute.get).post(alertContactsRoute.post).patch(alertContactsRoute.patch).delete(alertContactsRoute.del); Router.route('/alertcontacts').get(alertContactsRoute.get).post(alertContactsRoute.post).patch(alertContactsRoute.patch).delete(alertContactsRoute.del);
//Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patch(categoryRoute.patch).delete(categoryRoute.del); Router.route('/alertcontacts/describe').get(alertContactsRoute_schema);
// TODO: Migrate routes to lowercase.
//Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del);
//Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del);
//Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del);
Router.route('/version').get(versionRoute.get); Router.route('/version').get(versionRoute.get);
//Router.use('/search', search_routes); //Router.use('/search', search_routes);

View File

@ -1,109 +1,175 @@
_wrapperVersion = "1.0.0" _wrapperVersion = '1.0.0';
_minApiVersion = "1.0.0" _minApiVersion = '1.0.0';
_maxApiVersion = "1.0.0" _maxApiVersion = '1.0.0';
_defaultTTL = 60000;
_apiConfig = { _apiConfig = {
"basePath": "/api/v1/" basePath: '/api/v1/'
};
if (!window.localStorage) {
console.warn('Local Storage is not available, some features may not work');
} }
// Generic driver functions // Generic driver functions
let _api = { let _api = {
"get": async function(path) { get: async function (path) {
const options = { const options = {
headers: new Headers({'content-type': 'application/json'}) headers: new Headers({ 'content-type': 'application/json' })
}; };
const response = await fetch(_apiConfig.basePath + path, options) const response = await fetch(_apiConfig.basePath + path, options);
// Handle the response // Handle the response
if (!response.ok) { if (!response.ok) {
_testPageFail(response.statusText) _testPageFail(response.statusText);
return return;
} }
const result = await response.json() const result = await response.json();
// Handle the result, was json valid? // Handle the result, was json valid?
if (!result) { if (!result) {
_testPageFail("Invalid JSON response") _testPageFail('Invalid JSON response');
return return;
}
return result;
},
"post": async function(path, data) {
const options = {
method: 'POST',
headers: new Headers({'content-type': 'application/json'}),
body: JSON.stringify(data)
};
const response = await fetch(_apiConfig.basePath + path, options)
// Handle the response
if (!response.ok) {
_testPageFail(response.statusText)
return
}
const result = await response.json()
// Handle the result, was json valid?
if (!result) {
_testPageFail("Invalid JSON response")
return
} }
return result; return result;
}, },
"getAsync": function(path, callback) { post: async function (path, data) {
const options = { const options = {
headers: new Headers({'content-type': 'application/json'}) method: 'POST',
}; headers: new Headers({ 'content-type': 'application/json' }),
fetch(_apiConfig.basePath + path, options).then(response => response.json()).then(data => callback(data)).catch(error => _testPageFail(error)) body: JSON.stringify(data)
};
const response = await fetch(_apiConfig.basePath + path, options);
// Handle the response
if (!response.ok) {
_testPageFail(response.statusText);
return;
}
const result = await response.json();
// Handle the result, was json valid?
if (!result) {
_testPageFail('Invalid JSON response');
return;
}
return result;
},
getAsync: function (path, callback) {
const options = {
headers: new Headers({ 'content-type': 'application/json' })
};
fetch(_apiConfig.basePath + path, options)
.then((response) => response.json())
.then((data) => callback(data))
.catch((error) => _testPageFail(error));
}
};
function getApiDescriptionByTable(tableName) {
const keyDesc = `desc:${tableName}`;
const keyTime = `${keyDesc}:time`;
const keyTTL = `${keyDesc}:ttl`;
// Retrieve cached data
const description = JSON.parse(localStorage.getItem(keyDesc));
const timeCreated = parseInt(localStorage.getItem(keyTime));
const ttl = parseInt(localStorage.getItem(keyTTL));
// Check if valid cached data exists
if (description && timeCreated && ttl) {
const currentTime = Date.now();
const age = currentTime - parseInt(timeCreated, 10);
if (age < parseInt(ttl, 10)) {
// Return cached data immediately
return Promise.resolve(description);
} else {
console.warn('Cached description expired; fetching new data');
// Fetch new data, update cache, and return it
return fetchAndUpdateCache(tableName);
}
} else {
console.warn('No cached description; fetching from server');
// Fetch data, update cache, and return it
return fetchAndUpdateCache(tableName);
}
function fetchAndUpdateCache(tableName) {
return _api
.get(`${tableName}/describe`)
.then((data) => {
if (data) {
// Update local storage with new data
localStorage.setItem(keyDesc, JSON.stringify(data));
localStorage.setItem(keyTime, Date.now().toString());
localStorage.setItem(keyTTL, '60000'); // 60 seconds TTL
}
return data; // Return the fetched data
})
.catch((error) => {
console.error('Failed to fetch description:', error);
// Fallback to cached data if available (even if expired)
return description || null;
});
} }
} }
function returnTableDataByTableName(tableName) { function returnTableDataByTableName(tableName) {
return _api.get(tableName) return _api.get(tableName);
} }
function returnTableDataByTableNameWithSearch(tableName, search) { function returnTableDataByTableNameWithSearch(tableName, search) {
return _api.get(tableName + "?search=" + search) return _api.get(tableName + '?search=' + search);
} }
function returnTableDataByTableNameAsync(tableName, callback) { function returnTableDataByTableNameAsync(tableName, callback) {
_api.getAsync(tableName, callback) _api.getAsync(tableName, callback);
} }
async function getCountByTable(tableName) { async function getCountByTable(tableName) {
let result = await(_api.get(tableName + "?count")) // Stored in `data:count:${tableName}`
if(typeof result !== 'number') { let result = await _api.get(tableName + '?count=true');
_testPageWarn("Count was not a number, was: " + result) if (typeof result !== 'number') {
console.warn("Count was not a number, was: " + result) _testPageWarn('Count was not a number, was: ' + result);
return -1 console.warn('Count was not a number, was: ' + result);
return -1;
} }
return result return result;
} }
function getRowsByTableAndColumnList(tableName, columnList) { function getRowsByTableAndColumnList(tableName, columnList) {
//return _api.get(tableName + '/rows/' + columnList.join(',')) //return _api.get(tableName + '/rows/' + columnList.join(','))
return undefined return undefined;
} }
function _testPageFail(reason) { function _testPageFail(reason) {
document.getElementById("heroStatus").classList.remove("is-success") document.getElementById('heroStatus').classList.remove('is-success');
document.getElementById("heroStatus").classList.add("is-danger") document.getElementById('heroStatus').classList.add('is-danger');
document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Failed, reason: " + reason document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Failed, reason: ' + reason;
} }
function _testPageWarn(reason) { function _testPageWarn(reason) {
document.getElementById("heroStatus").classList.remove("is-success") document.getElementById('heroStatus').classList.remove('is-success');
document.getElementById("heroStatus").classList.add("is-warning") document.getElementById('heroStatus').classList.add('is-warning');
document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Warning, reason: " + reason document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Warning, reason: ' + reason;
} }
function getServerVersion() { function getServerVersion() {
return _api.get('version') return _api.get('version');
} }
function createEntry(tableName, data) { function createEntry(tableName, data) {
return _api.post(tableName, data) invalidateCache(tableName);
return _api.post(tableName, data);
} }
function invalidateCache(tableName) {
const keyDesc = `desc:${tableName}`;
const keyTime = `${keyDesc}:time`;
const keyTTL = `${keyDesc}:ttl`;
localStorage.removeItem(keyDesc);
localStorage.removeItem(keyTime);
localStorage.removeItem(keyTTL);
}

View File

@ -1,55 +1,55 @@
_pageDriverVersion = "1.0.1"; _pageDriverVersion = '1.0.1';
// Handle color for icon svg with id="logo" based on the current theme // Handle color for icon svg with id="logo" based on the current theme
const logo = document.getElementById("logo"); const logo = document.getElementById('logo');
if(logo) { if (logo) {
logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue("--bulma-text"); logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue('--bulma-text');
} }
if(_wrapperVersion === undefined) { if (_wrapperVersion === undefined) {
console.error("API Wrapper not found; Please include the API Wrapper before including the Page Driver"); console.error('API Wrapper not found; Please include the API Wrapper before including the Page Driver');
exit(); exit();
} else { } else {
console.log("API Wrapper found; Page Driver is ready to use"); console.log('API Wrapper found; Page Driver is ready to use');
} }
// Find all tables on the page which have data-dataSource attribute // Find all tables on the page which have data-dataSource attribute
var tables = document.querySelectorAll("table[data-dataSource]"); var tables = document.querySelectorAll('table[data-dataSource]');
//var tables = [] //var tables = []
// Get all single values with data-dataSource, data-dataCol and data-dataAction // Get all single values with data-dataSource, data-dataCol and data-dataAction
var singleValues = document.querySelectorAll("span[data-dataSource]"); var singleValues = document.querySelectorAll('span[data-dataSource]');
// Find all search fields with data-searchTargetId // Find all search fields with data-searchTargetId
var searchFields = document.querySelectorAll("input[data-searchTargetId]"); var searchFields = document.querySelectorAll('input[data-searchTargetId]');
// Find all modalForms // Find all modalForms
var modalForms = document.querySelectorAll("form[data-targetTable]"); var modalForms = document.querySelectorAll('form[data-targetTable]');
// Iterate over all tables // Iterate over all tables
tables.forEach(async table => { tables.forEach(async (table) => {
console.log("Table found: ", table); console.log('Table found: ', table);
// Get THEAD and TBODY elements // Get THEAD and TBODY elements
const thead = table.querySelector("thead"); const thead = table.querySelector('thead');
const tbody = table.querySelector("tbody"); const tbody = table.querySelector('tbody');
// get index per column // get index per column
const columns = thead.querySelectorAll("th"); const columns = thead.querySelectorAll('th');
const columnIndices = []; const columnIndices = [];
columns.forEach((column, index) => { columns.forEach((column, index) => {
columnIndices[column.getAttribute("data-dataCol")] = index; columnIndices[column.getAttribute('data-dataCol')] = index;
}); });
// All required cols // All required cols
let requiredCols = []; let requiredCols = [];
columns.forEach(column => { columns.forEach((column) => {
requiredCols.push(column.getAttribute("data-dataCol")); requiredCols.push(column.getAttribute('data-dataCol'));
}); });
console.log("Required columns: ", requiredCols); console.log('Required columns: ', requiredCols);
// Get data from API // Get data from API
//let result = getRowsByTableAndColumnList(table.getAttribute("data-dataSource"), requiredCols); //let result = getRowsByTableAndColumnList(table.getAttribute("data-dataSource"), requiredCols);
let result = await returnTableDataByTableName(table.getAttribute("data-dataSource")) let result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
// for (resultIndex in result) { // for (resultIndex in result) {
// const row = result[resultIndex]; // const row = result[resultIndex];
// const tr = document.createElement("tr"); // const tr = document.createElement("tr");
@ -62,122 +62,192 @@ tables.forEach(async table => {
// } // }
writeDataToTable(table, result); writeDataToTable(table, result);
console.log("Column indices: ", columnIndices); console.log('Column indices: ', columnIndices);
}); });
console.info("Processing single values"); console.info('Processing single values');
console.info(singleValues); console.info(singleValues);
// Iterate over all single values // Iterate over all single values
singleValues.forEach(async singleValue => { singleValues.forEach(async (singleValue) => {
writeSingelton(singleValue); writeSingelton(singleValue);
}); });
async function writeSingelton(element) { async function writeSingelton(element) {
const table = element.getAttribute("data-dataSource"); const table = element.getAttribute('data-dataSource');
console.log("Table: ", table, " Action: ", element.getAttribute("data-dataAction"), " Element: ", element); console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element);
switch(element.getAttribute("data-dataAction")) { switch (element.getAttribute('data-dataAction')) {
case "COUNT": { case 'COUNT': {
console.log("Count action found"); console.log('Count action found');
element.innerHTML = (await getCountByTable(table)) element.innerHTML = await getCountByTable(table);
break; break;
} }
case "SPECIAL": { case 'SPECIAL': {
if(table == "version") { if (table == 'version') {
element.innerHTML = (await getServerVersion())["version"]; element.innerHTML = (await getServerVersion())['version'];
break break;
} }
} }
default: { default: {
console.error("Unknown action found: ", element.getAttribute("data-dataAction")); console.error('Unknown action found: ', element.getAttribute('data-dataAction'));
break; break;
} }
} }
element.classList.remove("is-skeleton"); element.classList.remove('is-skeleton');
} }
// Attach listeners to search fields // Attach listeners to search fields
searchFields.forEach(searchField => { searchFields.forEach((searchField) => {
searchField.addEventListener("input", async function() { // Apply restrictions to search field (min, max, chars, etc)
console.log("Search field changed: ", searchField);
const targetId = searchField.getAttribute("data-searchTargetId"); getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => {
const target = document.getElementById(targetId); desc = desc['GET']['keys']['search'];
const table = target.getAttribute("data-dataSource"); var rules = desc['rules'];
const column = target.getAttribute("data-dataCol"); rules.forEach((rule) => {
const value = searchField.value; switch (rule['name']) {
console.log("Searching for ", value, " in ", table, " column ", column); case 'min': {
const result = await returnTableDataByTableNameWithSearch(table, value); searchField.setAttribute('minlength', rule['args']['limit']);
console.log("Result: ", result); break;
clearTable(target); }
writeDataToTable(target, result); case 'max': {
searchField.setAttribute('maxlength', rule['args']['limit']);
break;
}
}
});
});
searchField.addEventListener('input', async function () {
console.log('Search field changed: ', searchField);
if (searchField.checkValidity() == false) {
console.log('Invalid input');
searchField.classList.add('is-danger');
return;
} else {
searchField.classList.remove('is-danger');
const targetId = searchField.getAttribute('data-searchTargetId');
const target = document.getElementById(targetId);
const table = target.getAttribute('data-dataSource');
const column = target.getAttribute('data-dataCol');
const value = searchField.value;
console.log('Searching for ', value, ' in ', table, ' column ', column);
//const result = await returnTableDataByTableNameWithSearch(table, value);
//clearTable(target);
//writeDataToTable(target, result);
refreshTableByName(table);
}
}); });
}); });
// Attach listeners to modal forms // Attach listeners to modal forms
modalForms.forEach(modalForm => { modalForms.forEach((modalForm) => {
modalForm.addEventListener("submit", async function(event) { // Add validation to form by using API description (everything is assumed POST for now)
modalForm.addEventListener('input', async function (event) {
if (event.target.checkValidity() == false) {
modalForm.querySelector("input[type='submit']").setAttribute('disabled', true);
event.target.classList.add('is-danger');
return;
} else {
modalForm.querySelector("input[type='submit']").removeAttribute('disabled');
event.target.classList.remove('is-danger');
}
});
getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => {
console.log('Description: ', desc);
const keys = desc['POST']['keys'];
// Apply resitrictions and types to form fields
for (key in keys) {
const field = modalForm.querySelector("input[name='" + key + "']");
if (field) {
const rules = keys[key]['rules'];
const flags = keys[key]['flags'];
console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags);
rules.forEach((rule) => {
switch (rule['name']) {
case 'min': {
field.setAttribute('minlength', rule['args']['limit']);
break;
}
case 'max': {
field.setAttribute('maxlength', rule['args']['limit']);
break;
}
case 'pattern': {
field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1));
//field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$");
break;
}
case 'type': {
//field.setAttribute("type", rule["args"]["type"]);
console.log('Type: ', rule['args']['type']);
break;
}
}
});
if (flags) {
flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required');
}
}
}
console.log('Keys: ', keys);
});
modalForm.addEventListener('submit', async function (event) {
event.preventDefault(); event.preventDefault();
// Check what button submitted the form and if it has data-actionBtn = save // Check what button submitted the form and if it has data-actionBtn = save
// If not, close modal // If not, close modal
const pressedBtn = event.submitter; const pressedBtn = event.submitter;
if(pressedBtn.getAttribute("data-actionBtn") != "save") { if (pressedBtn.getAttribute('data-actionBtn') != 'save') {
modalForm.closest(".modal").classList.remove('is-active'); modalForm.closest('.modal').classList.remove('is-active');
return; return;
} }
// Find .entryPhase and hide it // Find .entryPhase and hide it
const entryPhase = modalForm.querySelector(".entryPhase"); const entryPhase = modalForm.querySelector('.entryPhase');
const loadPhase = modalForm.querySelector(".loadPhase"); const loadPhase = modalForm.querySelector('.loadPhase');
if(entryPhase) { if (entryPhase) {
entryPhase.classList.add("is-hidden"); entryPhase.classList.add('is-hidden');
} }
if(loadPhase) { if (loadPhase) {
loadPhase.classList.remove("is-hidden"); loadPhase.classList.remove('is-hidden');
} }
console.log("Form submitted: ", modalForm); console.log('Form submitted: ', modalForm);
const table = modalForm.getAttribute("data-targetTable"); const table = modalForm.getAttribute('data-targetTable');
const data = new FormData(modalForm); const data = new FormData(modalForm);
// Convert to JSON object // Convert to JSON object
let jsonData = {}; let jsonData = {};
data.forEach((value, key) => { data.forEach((value, key) => {
jsonData[key] = value; jsonData[key] = value;
}); });
console.log("JSON Data: ", jsonData); console.log('JSON Data: ', jsonData);
let resp = await createEntry(table, jsonData); let resp = await createEntry(table, jsonData);
console.log("Response: ", resp); console.log('Response: ', resp);
if(resp["status"] == "CREATED") { if (resp['status'] == 'CREATED') {
console.log("Entry created successfully"); console.log('Entry created successfully');
modalForm.closest(".modal").classList.remove('is-active'); modalForm.closest('.modal').classList.remove('is-active');
modalForm.reset(); modalForm.reset();
// Hide loadPhase // Hide loadPhase
if(loadPhase) { if (loadPhase) {
loadPhase.classList.add("is-hidden"); loadPhase.classList.add('is-hidden');
} }
// Show entryPhase // Show entryPhase
if(entryPhase) { if (entryPhase) {
entryPhase.classList.remove("is-hidden"); entryPhase.classList.remove('is-hidden');
} }
} else { } else {
// Hide loadPhase // Hide loadPhase
if(loadPhase) { if (loadPhase) {
loadPhase.classList.add("is-hidden"); loadPhase.classList.add('is-hidden');
} }
// Show entryPhase // Show entryPhase
if(entryPhase) { if (entryPhase) {
entryPhase.classList.remove("is-hidden"); entryPhase.classList.remove('is-hidden');
} }
// TODO: Show error message // TODO: Show error message
} }
// const target = document.getElementById(table); // const target = document.getElementById(table);
// writeDataToTable(target, result); // writeDataToTable(target, result);
// Find all tables with data-searchTargetId set to table // Find all tables with data-searchTargetId set to table
setTimeout(() => { setTimeout(() => {
@ -191,14 +261,14 @@ modalForms.forEach(modalForm => {
async function refreshTable(table) { async function refreshTable(table) {
// Refresh a table while keeping (optionally set) search value // Refresh a table while keeping (optionally set) search value
const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']"); const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']");
if(searchField) { if (searchField && searchField.value != '') {
const value = searchField.value; const value = searchField.value;
const dbTable = table.getAttribute("data-dataSource"); const dbTable = table.getAttribute('data-dataSource');
const result = await returnTableDataByTableNameWithSearch(dbTable, value); const result = await returnTableDataByTableNameWithSearch(dbTable, value);
clearTable(table); clearTable(table);
writeDataToTable(table, result); writeDataToTable(table, result);
} else { } else {
const result = await returnTableDataByTableName(table.getAttribute("data-dataSource")); const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
clearTable(table); clearTable(table);
writeDataToTable(table, result); writeDataToTable(table, result);
} }
@ -206,48 +276,47 @@ async function refreshTable(table) {
async function refreshTableByName(name) { async function refreshTableByName(name) {
const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']"); const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']");
for(dirty of dirtyTables) { for (dirty of dirtyTables) {
refreshTable(dirty); refreshTable(dirty);
} }
} }
async function updateSingeltonsByTableName(name) { async function updateSingeltonsByTableName(name) {
const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']"); const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']");
for(dirty of dirtySingles) { for (dirty of dirtySingles) {
writeSingelton(dirty); writeSingelton(dirty);
} }
} }
function clearTable(table) { function clearTable(table) {
const tbody = table.querySelector("tbody"); const tbody = table.querySelector('tbody');
tbody.innerHTML = ""; tbody.innerHTML = '';
} }
function writeDataToTable(table, data) { function writeDataToTable(table, data) {
console.log("Writing data to table: ", table, data); console.log('Writing data to table: ', table, data);
// Get THEAD and TBODY elements // Get THEAD and TBODY elements
const thead = table.querySelector("thead"); const thead = table.querySelector('thead');
const tbody = table.querySelector("tbody"); const tbody = table.querySelector('tbody');
// get index per column // get index per column
const columns = thead.querySelectorAll("th"); const columns = thead.querySelectorAll('th');
const columnIndices = []; const columnIndices = [];
columns.forEach((column, index) => { columns.forEach((column, index) => {
columnIndices[column.getAttribute("data-dataCol")] = index; columnIndices[column.getAttribute('data-dataCol')] = index;
}); });
// All required cols // All required cols
let requiredCols = []; let requiredCols = [];
columns.forEach(column => { columns.forEach((column) => {
requiredCols.push(column.getAttribute("data-dataCol")); requiredCols.push(column.getAttribute('data-dataCol'));
}); });
for (resultIndex in data) { for (resultIndex in data) {
const row = data[resultIndex]; const row = data[resultIndex];
const tr = document.createElement("tr"); const tr = document.createElement('tr');
requiredCols.forEach(column => { requiredCols.forEach((column) => {
const td = document.createElement("td"); const td = document.createElement('td');
td.innerHTML = row[column]; td.innerHTML = row[column];
tr.appendChild(td); tr.appendChild(td);
}); });
@ -255,47 +324,46 @@ function writeDataToTable(table, data) {
} }
} }
// Handle modal
// Handle modal
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Functions to open and close a modal // Functions to open and close a modal
function openModal($el) { function openModal($el) {
$el.classList.add('is-active'); $el.classList.add('is-active');
} }
function closeModal($el) { function closeModal($el) {
$el.classList.remove('is-active'); $el.classList.remove('is-active');
} }
function closeAllModals() { function closeAllModals() {
(document.querySelectorAll('.modal') || []).forEach(($modal) => { (document.querySelectorAll('.modal') || []).forEach(($modal) => {
closeModal($modal); closeModal($modal);
}); });
} }
// Add a click event on buttons to open a specific modal // Add a click event on buttons to open a specific modal
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { (document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
const modal = $trigger.dataset.target; const modal = $trigger.dataset.target;
const $target = document.getElementById(modal); const $target = document.getElementById(modal);
$trigger.addEventListener('click', () => { $trigger.addEventListener('click', () => {
openModal($target); openModal($target);
}); });
}); });
// Add a click event on various child elements to close the parent modal // Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { (document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
const $target = $close.closest('.modal'); const $target = $close.closest('.modal');
$close.addEventListener('click', () => { $close.addEventListener('click', () => {
closeModal($target); closeModal($target);
}); });
}); });
// Add a keyboard event to close all modals // Add a keyboard event to close all modals
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
if(event.key === "Escape") { if (event.key === 'Escape') {
closeAllModals(); closeAllModals();
} }
}); });
}); });

View File

@ -35,67 +35,68 @@
<!-- TODO: Mark required fields as required; add handling for validation --> <!-- TODO: Mark required fields as required; add handling for validation -->
<div id="modal-js-example" class="modal"> <div id="modal-js-example" class="modal">
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-content"> <div class="modal-content">
<div class="box entryPhase is-hidden"> <div class="box entryPhase is-hidden">
<h2 class="title">New Contact</h1> <h2 class="title">New Contact</h1>
<i class="bi bi-arrow-clockwise title"></i> <i class="bi bi-arrow-clockwise title"></i>
</div> </div>
<div class="box entryPhase"> <div class="box entryPhase">
<h2 class="title">New Contact</h1> <h2 class="title">New Contact</h1>
<form data-targetTable="AlertContacts"> <form data-targetTable="AlertContacts">
<div class="field"> <div class="field">
<label class="label">Name</label> <label class="label">Name</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" placeholder="John Doe" value="" name="name"> <input class="input" type="text" placeholder="John Doe" value="" name="name">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="bi bi-file-earmark-person-fill"></i> <i class="bi bi-file-earmark-person-fill"></i>
</span> </span>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Telephone</label> <label class="label">Telephone</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone"> <input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="bi bi-telephone-fill"></i> <i class="bi bi-telephone-fill"></i>
</span> </span>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Comment</label> <label class="label">Comment</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" placeholder="" value="" name="comment"> <input class="input" type="text" placeholder="" value="" name="comment">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="bi bi-chat-fill"></i> <i class="bi bi-chat-fill"></i>
</span> </span>
</div> </div>
</div> </div>
<br>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<input type="submit" class="button is-link" value="Save" data-actionBtn="save"> <input type="submit" class="button is-link" value="Save" data-actionBtn="save">
</div> </div>
<div class="control"> <!--<div class="control">
<button class="button is-link is-light" data-actionBtn="cancel">Cancel</button> <button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button>
</div>-->
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>
<button class="modal-close is-large" aria-label="close"></button> <button class="modal-close is-large" aria-label="close"></button>
</div> </div>
<button class="js-modal-trigger button" data-target="modal-js-example"> <button class="js-modal-trigger button" data-target="modal-js-example">
Create new Contact Create new Contact
</button> </button>
<section class="section"> <section class="section">
<h1 class="title" data-tK="start-recent-header">Alarm Kontakte</h1> <h1 class="title" data-tK="start-recent-header">Alarm Kontakte</h1>