mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-25 17:15:09 +00:00
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:
parent
e7208eaa1a
commit
0e469f2e67
@ -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
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -33,6 +33,7 @@
|
||||
notifyOnChangeSetRequiresReview=(action "onCompliancePolicyChangeSetDrift")
|
||||
onSave=(action "savePrivacyCompliancePolicy")
|
||||
onReset=(action "resetPrivacyCompliancePolicy")
|
||||
onComplianceUpload=(action "onComplianceUpload")
|
||||
}}
|
||||
|
||||
{{/if}}
|
||||
|
10
wherehows-web/app/typings/generic.d.ts
vendored
10
wherehows-web/app/typings/generic.d.ts
vendored
@ -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 };
|
||||
|
116
wherehows-web/app/utils/datasets/compliance/metadata-schema.ts
Normal file
116
wherehows-web/app/utils/datasets/compliance/metadata-schema.ts
Normal 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 };
|
Loading…
x
Reference in New Issue
Block a user