Data Validator Helper v2

Data Validator Helper

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name					Data Validator Helper v2
// @namespace			http://tampermonkey.net/
// @version				0.2
// @description		Data Validator Helper
// @author				Eric Liu <[email protected]>
// @match					https://data.veracity.com/dataValidator/*
// @match					https://datatest.veracity.com/dataValidator/*
// @match					https://datadevtest.veracity.com/dataValidator/*
// @grant					GM.xmlHttpRequest
// @grant					GM.registerMenuCommand
// ==/UserScript==

const baseUrl = location.origin + '/api/dataValidator';

(function () {
	'use strict';

	window._historyWrap = function (type) {
		var orig = history[type];
		var e = new Event(type.toLowerCase());

		return function () {
			var rv = orig.apply(this, arguments);
			e.arguments = arguments;
			window.dispatchEvent(e);
			return rv;
		};
	};
	history.pushState = _historyWrap('pushState');
	history.replaceState = _historyWrap('replaceState');

	window.schema = null;
	window.schemas = [];
	window.columnValidators = [];
	initialize();
	window.addEventListener('pushstate', (e) => {
		window.setTimeout(onRouteChange, 500);
	});
})();

async function initialize() {
	await onRouteChange();
}

async function onRouteChange() {
	if (isSchemaListPage()) {
		console.log('schema list');
		await loadColumnValidators();
		await loadSchemas();

		await createConvertButton();
		await createSchemaDeleteButtons();
	} else if (isSchemaPage()) {
		console.log('schema');
		await loadSchema();

		await createDeleteVersionButton();
	}
}

function isSchemaListPage() {
	// path = /dataValidator/08905150-2e46-4db0-b9c2-52dc6c376d29/schemas
	const path = document.location.pathname;
	return path.length == 59 && path.endsWith('/schemas');
}

function isSchemaPage() {
	// path = /dataValidator/08905150-2e46-4db0-b9c2-52dc6c376d29/schemas/939bea1f-a595-4218-b31f-834a84aef632
	const path = document.location.pathname;
	return path.length == 96 && path.slice(51, 59).endsWith('/schemas');
}

function findElementByXPath(xpath, interval = 100) {
	return new Promise((resolve) => {
		window.__findElementByXPath = window.setInterval(() => {
			const element = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null).iterateNext();

			if (element) {
				console.log('findElementByXPath', element);
				resolve(element);
				window.clearInterval(window.__findElementByXPath);
			}
		}, interval);
	});
}

function findElement(selector, interval = 100) {
	return new Promise((resolve) => {
		window.intervalHandler = window.setInterval(() => {
			const element = document.querySelector(selector);
			resolve(element);
			window.clearInterval(window.intervalHandler);
		}, interval);
	});
}

function findElements(selector, interval = 100) {
	return new Promise((resolve) => {
		window.intervalHandler = window.setInterval(() => {
			const elements = [...document.querySelectorAll(selector)];
			resolve(elements);
			window.clearInterval(window.intervalHandler);
		}, interval);
	});
}

async function createConvertButton() {
	const addButton = await findElement('#addSchemaButton');

	if (!addButton) return;

	const container = addButton.parentElement;
	let button = document.createElement('button');
	button.innerText = 'Create Schema from IMT (json)';
	button.id = 'convertButton';
	button.style.borderRadius = '5px';
	button.style.marginLeft = '5px';
	button.style.backgroundColor = 'pink';
	button.addEventListener('click', createSchemaFromIMT);
	container.appendChild(button);

	button = document.createElement('button');
	button.innerText = 'Copy from another schema';
	button.style.borderRadius = '5px';
	button.style.marginLeft = '5px';
	button.style.backgroundColor = 'pink';
	button.addEventListener('click', copySchema);
	container.appendChild(button);
}

async function createSchemaDeleteButtons() {
	const list = await findElements('ul#sortedSchemasList > li');

	if (list.length == 0) return;

	for (let r of list) {
		const name = r.querySelector('span:first-child').innerText.trim();
		const container = r.querySelector('div:last-child');
		const button = document.createElement('button');
		button.innerText = 'Delete';
		button.style.borderRadius = '5px';
		button.style.marginLeft = '5px';
		button.style.backgroundColor = 'pink';
		button.addEventListener('click', (e) => {
			e.preventDefault();
			e.cancelBubble = true;
			deleteSchema(name);
		});
		container.appendChild(button);
	}
}

async function createDeleteVersionButton() {
	const targetButton = await findElementByXPath('//button[contains(text(), "Activate version")]');

	if (!targetButton) return;

	const container = targetButton.parentElement;

	let button = document.createElement('button');
	button.id = 'version-delete-button';
	button.innerText = 'Delete version';
	button.style = 'height: 40px; background-color: pink; border-radius: 5px';
	button.addEventListener('click', (e) => deleteSchemaVersion(container));
	container.appendChild(button);
}

async function deleteSchema(name) {
	function del(workspaceId, schemaId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/schemas/${schemaId}?delete`,
				method: 'DELETE',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: () => resolve(),
				onerror: (err) => reject(err)
			})
		);
	}
	const schemas = window.schemas;
	const schema = schemas.find((s) => s.name == name);

	if (schema) {
		if (confirm(`Delete schema ${name}?`)) {
			const workspaceId = getWorkspaceId();
			await del(workspaceId, schema.schemaId);
			location.assign(location.href);
		}
	}
}

async function deleteSchemaVersion(container) {
	function del(workspaceId, versionId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/schemaVersions/${versionId}?delete`,
				method: 'DELETE',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: () => resolve(),
				onerror: (err) => reject(err)
			})
		);
	}

	const versionText = container.firstChild.querySelector('button').textContent;
	const version = parseInt(versionText.split(' ')[1]);
	const schema = window.schema;
	const schemaVersion = schema.versions.find((v) => v.version == version);

	if (schemaVersion) {
		if (schemaVersion.isActive) {
			alert('cannot delete active version');
			return;
		}

		if (confirm(`Delete version ${version}?`)) {
			const workspaceId = getWorkspaceId();
			await del(workspaceId, schemaVersion.schemaVersionId);
			location.assign(location.href);
		}
	}
}

function getWorkspaceId() {
	const path = document.location.pathname;
	return (path.length >= 51 && path.slice(15, 51)) || null;
}

function getSchemaId() {
	const path = document.location.pathname;
	return (path.length >= 96 && path.slice(60, 96)) || null;
}

async function createSchemaFromIMT() {
	const workspaceId = getWorkspaceId();
	const shortName = prompt('Enter the short name of your schema');
	const input = prompt('JSON from IMT');

	if (!workspaceId || !shortName?.trim() || !input?.trim()) return;

	const imt = JSON.parse(input);
	const schema = imt2dv(shortName, shortName, shortName, imt);
	const result = await createSchema(workspaceId, schema);

	if (result) {
		location.assign(location.pathname);
	}
}

async function copySchema() {
	function get(workspaceId, schemaId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/schemas/${schemaId}`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	function getValidators(workspaceId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/validators`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	function createValidator(workspaceId, validator) {
		validator.name += '1';
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/validators`,
				method: 'POST',
				data: JSON.stringify(validator),
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	const workspaceId = getWorkspaceId();
	const otherWorkspaceId = prompt('The other workspaceId');
	const otherSchemaId = prompt('The other schemaId');
	const shortName = prompt('Enter the short name of new schema');

	if (!workspaceId || !otherWorkspaceId || !otherSchemaId || !shortName?.trim()) return;

	const otherSchema = await get(otherWorkspaceId, otherSchemaId);
	const otherValidators = await getValidators(otherWorkspaceId);
	const validators = [...window.columnValidators];

	for (let ov of otherValidators) {
		if (validators.find((v) => v.name == ov.name)) continue;

		const newValidator = await createValidator(workspaceId, ov);
		validators.push(newValidator);
	}

	const schema = schemaReadToWrite(otherSchema, validators);

	if (!schema) {
		alert('Error');
		return;
	}

	const result = await createSchema(workspaceId, schema);

	if (result) {
		location.assign(location.pathname);
	}
}

async function loadSchemas() {
	function get(workspaceId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/schemas`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	const workspaceId = getWorkspaceId();
	const schemas = await get(workspaceId);
	window.schemas = schemas;
}

async function loadSchema() {
	function get(workspaceId, schemaId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/schemas/${schemaId}`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	const [workspaceId, schemaId] = [getWorkspaceId(), getSchemaId()];
	const schema = await get(workspaceId, schemaId);
	window.schema = schema;
}

async function loadColumnValidators() {
	function getPredefined() {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/validators`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => resolve(JSON.parse(res.response)),
				onerror: (err) => reject(err)
			})
		);
	}

	function get(workspaceId) {
		return new Promise((resolve, reject) =>
			GM.xmlHttpRequest({
				url: `${baseUrl}/workspaces/${workspaceId}/validators`,
				method: 'GET',
				headers: {
					'Content-Type': 'application/json'
				},
				onload: (res) => {
					console.log(res.response);
					resolve(JSON.parse(res.response));
				},
				onerror: (err) => reject(err)
			})
		);
	}

	try {
		const v1 = (await getPredefined()) || [];
		const v2 = (await get(getWorkspaceId())) || [];
		const v = [];
		v.push(...v1);
		v.push(...v2);
		window.columnValidators = v;
	} catch (e) {
		console.error(e);
	}
}

function createSchema(workspaceId, schema) {
	return new Promise((resolve, reject) => {
		GM.xmlHttpRequest({
			url: `${baseUrl}/workspaces/${workspaceId}/schemas`,
			method: 'POST',
			data: JSON.stringify(schema),
			headers: {
				'Content-Type': 'application/json'
			},
			onload: (res) => resolve(JSON.parse(res.response)),
			onerror: (err) => reject(err)
		});
	});
}

function imt2dv(name, shortName, description, imt) {
	const columnValidators = window.columnValidators;

	function getColumnValidator(name) {
		if (columnValidators) {
			return columnValidators.find((v) => v.name == name);
		}
	}
	function convertColumnValidators(imt) {
		return Object.keys(imt).map((k) => {
			const columnValidatorId = getColumnValidator(k).columnValidatorId;
			const severity = imt[k] == 0 ? 'Error' : imt[k] == 1 ? 'Warning' : 'Correction';
			return { columnValidatorId, severity };
		});
	}
	function convertColumns(imt) {
		return imt.map((c, i) => ({
			name: c.Src,
			mustBePresent: c.MustBePresent,
			columnOrder: i + 1,
			metaFormat: c.Format,
			schemaColumnType: c.Meta ? 'Metadata' : 'Validation',
			metaType: 'NotApplicable',
			columnValidators: convertColumnValidators(c.Rules)
		}));
	}
	function convertRowValidators(imt) {
		return Object.keys(imt).map((k) => {
			const v = imt[k];
			return {
				name: k,
				checkParameter: v.CheckParameter || '',
				ogicalOperator: v.RulesLogicalOperator,
				rowValidatorType: v.Type,
				columnsToCheck: v.ColsToCheck,
				columnsToAffect: v.ColsToAffect
			};
		});
	}

	const schemaColumns = convertColumns(imt.ColumnsOrder);
	const rowValidators = convertRowValidators(imt.Row);
	return {
		name,
		shortName,
		description,
		version: { schemaColumns, rowValidators }
	};
}

function schemaReadToWrite(schema, columnValidators) {
	schema.version = schema.activeVersion;

	for (let key of ['schemaId', 'isPredefined', 'workspaceId', 'activeVersion', 'versions']) {
		delete schema[key];
	}

	for (let key of ['schemaVersionId', 'schemaId', 'version', 'isActive']) {
		delete schema.version[key];
	}

	for (let column of schema.version.schemaColumns) {
		for (let key of ['schemaColumnId', 'schemaVersionId']) {
			delete column[key];
		}

		for (let validator of column.columnValidators) {
			const v = columnValidators.find(
				(v) => v.name == validator.validatorName || v.name == validator.validatorName + '1'
			);

			if (!v) {
				console.error(validator);
				return undefined;
			}

			validator.columnValidatorId = v.columnValidatorId;

			for (let key of ['schemaColumnId', 'validatorName']) {
				delete validator[key];
			}
		}
	}

	for (let validator of schema.version.rowValidators) {
		for (let key of ['rowValidatorId', 'schemaVersionId']) {
			delete validator[key];
		}
		validator.columnsToCheck = validator.columnsToCheck.map((c) => c.columnName);
		validator.columnsToAffect = validator.columnsToAffect.map((c) => c.columnName);
	}

	return schema;
}