updates schema for json metadata. initial refactor of function to validate metadata schema properties. refactors action handler for upload: moves to container component

This commit is contained in:
Seyi Adebajo 2018-05-23 13:34:53 -07:00
parent e7208eaa1a
commit 0e469f2e67
6 changed files with 185 additions and 43 deletions

View File

@ -35,7 +35,6 @@ import {
singleTagsInChangeSet,
tagsForIdentifierField
} from 'wherehows-web/constants';
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/compliance-policy';
import { getTagsSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions';
import { arrayMap, compact, isListUnique, iterateArrayAsync } from 'wherehows-web/utils/array';
import noop from 'wherehows-web/utils/noop';
@ -67,15 +66,12 @@ import { IdLogicalType, NonIdLogicalType } from 'wherehows-web/constants/dataset
import { pick } from 'lodash';
import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
import { action } from '@ember-decorators/object';
const {
complianceDataException,
complianceFieldNotUnique,
missingTypes,
helpText,
successUploading,
invalidPolicyData,
missingPurgePolicy,
missingDatasetSecurityClassification
} = compliancePolicyStrings;
@ -128,6 +124,9 @@ export default class DatasetCompliance extends Component {
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>;
onReset: <T>() => Promise<T>;
onSave: <T>() => Promise<T>;
onComplianceUpload: (jsonString: string) => void;
notifyOnChangeSetSuggestions: (hasSuggestions: boolean) => void;
notifyOnChangeSetRequiresReview: (hasChangeSetDrift: boolean) => void;
@ -816,9 +815,9 @@ export default class DatasetCompliance extends Component {
* checked in the function. If criteria is not met, an the returned promise is settled
* in a rejected state, otherwise fulfilled
* @method
* @return {Promise<never> | void}
* @return {Promise<never | void>}
*/
validateFields(this: DatasetCompliance): Promise<never> | void {
async validateFields(this: DatasetCompliance): Promise<never | void> {
const { notify } = get(this, 'notifications');
const { complianceEntities = [] } = get(this, 'complianceInfo') || {};
const idTypeComplianceEntities = complianceEntities.filter(isTagIdType(get(this, 'complianceDataTypes')));
@ -891,39 +890,6 @@ export default class DatasetCompliance extends Component {
get(this, 'updateEditStepTask').perform();
}
/**
* Receives the json representation for compliance and applies each key to the policy
* @param {string} textString string representation for the JSON file
*/
@action
async onComplianceJsonUpload(this: DatasetCompliance, textString: string): Promise<void> {
const {
complianceInfo,
notifications: { notify }
} = getProperties(this, ['complianceInfo', 'notifications']);
if (complianceInfo) {
try {
const policy = JSON.parse(textString);
if (isPolicyExpectedShape(policy)) {
setProperties(complianceInfo, {
complianceEntities: policy.complianceEntities,
datasetClassification: policy.datasetClassification
});
notify(NotificationEvent.info, {
content: successUploading
});
}
} catch (e) {
notify(NotificationEvent.error, {
content: invalidPolicyData
});
}
}
}
actions: IDatasetComplianceActions = {
/**
* Action handles wizard step cancellation
@ -1230,6 +1196,14 @@ export default class DatasetCompliance extends Component {
anchor.click();
},
/**
* Receives the json representation for compliance and applies each key to the policy
* @param {string} jsonString string representation for the JSON file
*/
onComplianceJsonUpload(this: DatasetCompliance, jsonString: string): void {
get(this, 'onComplianceUpload')(jsonString);
},
/**
* Updates the source object representing the current datasetClassification map
* @param {keyof typeof DatasetClassifiers} classifier the property on the datasetClassification to update

View File

@ -1,5 +1,5 @@
import Component from '@ember/component';
import { get, set, setProperties } from '@ember/object';
import { get, set, setProperties, getProperties } from '@ember/object';
import ComputedProperty from '@ember/object/computed';
import { inject } from '@ember/service';
import { task } from 'ember-concurrency';
@ -28,6 +28,10 @@ import {
SuggestionIntent
} from 'wherehows-web/constants';
import { iterateArrayAsync } from 'wherehows-web/utils/array';
import validateMetadataObject, {
datasetComplianceMetadataTaxonomy
} from 'wherehows-web/utils/datasets/compliance/metadata-schema';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
/**
* Type alias for the response when container data items are batched
@ -52,7 +56,7 @@ type BatchContainerDataResult = Pick<
| 'schemaless'
>;
const { successUpdating, failedUpdating } = compliancePolicyStrings;
const { successUpdating, failedUpdating, successUploading, invalidPolicyData } = compliancePolicyStrings;
export default class DatasetComplianceContainer extends Component {
/**
@ -336,4 +340,43 @@ export default class DatasetComplianceContainer extends Component {
onSuggestionsComplianceFeedback(uid: string | null = null, feedback: SuggestionIntent) {
saveDatasetComplianceSuggestionFeedbackByUrn(get(this, 'urn'), uid, feedback);
}
/**
* Reapplies the uploaded compliance policy to the container property
* @param {string} jsonString string representation for the JSON file
* @memberof DatasetComplianceContainer
*/
@action
onComplianceUpload(this: DatasetComplianceContainer, jsonString: string): void {
const {
complianceInfo,
notifications: { notify }
} = getProperties(this, ['complianceInfo', 'notifications']);
if (complianceInfo) {
try {
const policy = JSON.parse(jsonString);
if (validateMetadataObject(policy, datasetComplianceMetadataTaxonomy)) {
const { complianceEntities, datasetClassification } = policy;
const resolvedComplianceInfo = { ...complianceInfo, complianceEntities, datasetClassification };
const { dialogActions } = notificationDialogActionFactory();
set(this, 'complianceInfo', resolvedComplianceInfo);
notify(NotificationEvent.confirm, {
header: 'Successfully applied uploaded metadata',
content: successUploading,
dialogActions,
dismissButtonText: false,
confirmButtonText: 'Dismiss'
});
}
} catch (e) {
notify(NotificationEvent.error, {
content: invalidPolicyData
});
}
}
}
}

View File

@ -34,7 +34,7 @@ const compliancePolicyStrings = {
missingTypes: 'Looks like you may have forgotten to specify a `Field Format` for all ID fields?',
successUpdating: 'Changes have been successfully saved!',
failedUpdating: 'An error occurred while saving.',
successUploading: 'Metadata successfully updated! Please "Save" when ready.',
successUploading: 'Metadata successfully updated! Please confirm and "Save" when ready.',
invalidPolicyData: 'Received policy in an unexpected format! Please check the provided attributes and try again.',
helpText: {
classification:

View File

@ -33,6 +33,7 @@
notifyOnChangeSetRequiresReview=(action "onCompliancePolicyChangeSetDrift")
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")
onComplianceUpload=(action "onComplianceUpload")
}}
{{/if}}

View File

@ -10,4 +10,12 @@ type StringUnionKeyToValue<U extends string> = { [K in U]: K };
*/
type StringEnumKeyToEnumValue<T extends string, V> = { [K in T]: V };
export { StringUnionKeyToValue, StringEnumKeyToEnumValue };
/**
* Describes the index signature for a generic object
* @interface IObject
*/
interface IObject<T> {
[K: string]: T;
}
export { StringUnionKeyToValue, StringEnumKeyToEnumValue, IObject };

View File

@ -0,0 +1,116 @@
import { typeOf } from '@ember/utils';
import { DatasetClassifiers } from 'wherehows-web/constants';
import { arrayEvery, arrayReduce } from 'wherehows-web/utils/array';
import { Classification } from 'wherehows-web/constants/datasets/compliance';
import { IObject } from 'wherehows-web/typings/generic';
/**
* Describes the interface for schemas that are used for compliance metadata objects
* @interface IMetadataTaxonomy
*/
interface IMetadataTaxonomy {
readonly '@props': Array<string>;
readonly '@type': string;
readonly [K: string]: IMetadataTaxonomy | any;
}
/**
* Defines the shape of the dataset compliance metadata json object using the IMetadataTaxonomy interface
* @type {({'@type': string; '@props': string[]} | {'@type': string; '@props': string[]; securityClassification: {'@type': string; '@props': any[]}})[]}
*/
const datasetComplianceMetadataTaxonomy: Array<IMetadataTaxonomy> = [
{
'@type': 'datasetClassification:object',
'@props': Object.keys(DatasetClassifiers).map(key => `${key}:boolean`)
},
{
'@type': 'complianceEntities:array',
'@props': [
'identifierField:string',
'identifierType:string|null',
'securityClassification:string|null',
'logicalType:string|null',
'nonOwner:boolean|null',
'valuePattern:string|null'
],
securityClassification: {
'@type': 'securityClassification:string',
'@props': Object.values(Classification)
}
}
];
/**
* Checks that a value type matches an expected pattern string
* @param {*} value the value to check
* @param {string} expectedTypePattern the pattern string to match against
* @returns {boolean}
*/
const valueEquiv = (value: any, expectedTypePattern: string): boolean => expectedTypePattern.includes(typeOf(value));
/**
* Extracts the type key and the pattern string from the string mapping into a tuple pair
* @param {string} objectKeyTypePattern string value consisting of a pair of key/property name and allowed types separated by a colon ":"
* @returns {[string, string]}
*/
const typePatternMap = (objectKeyTypePattern: string): [string, string] =>
<[string, string]>objectKeyTypePattern.split(':');
/**
* Returns a iteratee bound to an object that checks that a key matches the expected value in the typeMap
* @param {IObject<any>} object the object with keys to check
* @return {(typeMap: string) => boolean}
*/
const keyValueHasMatch = (object: IObject<any>) => (typeMap: string) => {
const [key, typeString] = typePatternMap(typeMap);
return valueEquiv(object[key], typeString);
};
/**
* Checks each key on an object matches the expected types in the typeMap
* @param {IObject<any>} object the object with keys to check
* @param {Array<string>} typeMaps the colon delimited type string
* @returns {boolean}
*/
const keysEquiv = (object: IObject<any>, typeMaps: Array<string>): boolean =>
arrayEvery(keyValueHasMatch(object))(typeMaps);
/**
* Checks that a compliance metadata object has a schema that matches the taxonomy / schema provided
* @param {IObject<any>} object an instance of a compliance metadata object
* @param {Array<IMetadataTaxonomy>} taxonomy schema shape to check against
* @return {boolean}
*/
const validateMetadataObject = (object: IObject<any>, taxonomy: Array<IMetadataTaxonomy>): boolean => {
const rootTypeMaps = taxonomy.map(category => category['@type']);
let isValid = keysEquiv(object, rootTypeMaps);
if (isValid) {
const downlevelAccumulator = (validity: boolean, typeMap: string): boolean => {
const [key, pattern]: [string, string] = typePatternMap(typeMap);
if (pattern.includes('object')) {
validity = keysEquiv(object[key], taxonomy.findBy('@type', typeMap)!['@props']);
}
if (pattern.includes('array') && Array.isArray(object[key])) {
validity = arrayReduce(
(validity: boolean, value: IObject<string>) =>
validity && keysEquiv(value, taxonomy.findBy('@type', typeMap)!['@props']),
validity
)(object[key]);
}
return validity;
};
return arrayReduce(downlevelAccumulator, isValid)(rootTypeMaps);
}
return isValid;
};
export default validateMetadataObject;
export { datasetComplianceMetadataTaxonomy };