mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-26 01:23:16 +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,
|
singleTagsInChangeSet,
|
||||||
tagsForIdentifierField
|
tagsForIdentifierField
|
||||||
} from 'wherehows-web/constants';
|
} from 'wherehows-web/constants';
|
||||||
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/compliance-policy';
|
|
||||||
import { getTagsSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions';
|
import { getTagsSuggestions } from 'wherehows-web/utils/datasets/compliance-suggestions';
|
||||||
import { arrayMap, compact, isListUnique, iterateArrayAsync } from 'wherehows-web/utils/array';
|
import { arrayMap, compact, isListUnique, iterateArrayAsync } from 'wherehows-web/utils/array';
|
||||||
import noop from 'wherehows-web/utils/noop';
|
import noop from 'wherehows-web/utils/noop';
|
||||||
@ -67,15 +66,12 @@ import { IdLogicalType, NonIdLogicalType } from 'wherehows-web/constants/dataset
|
|||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
|
import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
|
||||||
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
|
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
|
||||||
import { action } from '@ember-decorators/object';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
complianceDataException,
|
complianceDataException,
|
||||||
complianceFieldNotUnique,
|
complianceFieldNotUnique,
|
||||||
missingTypes,
|
missingTypes,
|
||||||
helpText,
|
helpText,
|
||||||
successUploading,
|
|
||||||
invalidPolicyData,
|
|
||||||
missingPurgePolicy,
|
missingPurgePolicy,
|
||||||
missingDatasetSecurityClassification
|
missingDatasetSecurityClassification
|
||||||
} = compliancePolicyStrings;
|
} = compliancePolicyStrings;
|
||||||
@ -128,6 +124,9 @@ export default class DatasetCompliance extends Component {
|
|||||||
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>;
|
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>;
|
||||||
onReset: <T>() => Promise<T>;
|
onReset: <T>() => Promise<T>;
|
||||||
onSave: <T>() => Promise<T>;
|
onSave: <T>() => Promise<T>;
|
||||||
|
|
||||||
|
onComplianceUpload: (jsonString: string) => void;
|
||||||
|
|
||||||
notifyOnChangeSetSuggestions: (hasSuggestions: boolean) => void;
|
notifyOnChangeSetSuggestions: (hasSuggestions: boolean) => void;
|
||||||
notifyOnChangeSetRequiresReview: (hasChangeSetDrift: 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
|
* checked in the function. If criteria is not met, an the returned promise is settled
|
||||||
* in a rejected state, otherwise fulfilled
|
* in a rejected state, otherwise fulfilled
|
||||||
* @method
|
* @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 { notify } = get(this, 'notifications');
|
||||||
const { complianceEntities = [] } = get(this, 'complianceInfo') || {};
|
const { complianceEntities = [] } = get(this, 'complianceInfo') || {};
|
||||||
const idTypeComplianceEntities = complianceEntities.filter(isTagIdType(get(this, 'complianceDataTypes')));
|
const idTypeComplianceEntities = complianceEntities.filter(isTagIdType(get(this, 'complianceDataTypes')));
|
||||||
@ -891,39 +890,6 @@ export default class DatasetCompliance extends Component {
|
|||||||
get(this, 'updateEditStepTask').perform();
|
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 = {
|
actions: IDatasetComplianceActions = {
|
||||||
/**
|
/**
|
||||||
* Action handles wizard step cancellation
|
* Action handles wizard step cancellation
|
||||||
@ -1230,6 +1196,14 @@ export default class DatasetCompliance extends Component {
|
|||||||
anchor.click();
|
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
|
* Updates the source object representing the current datasetClassification map
|
||||||
* @param {keyof typeof DatasetClassifiers} classifier the property on the datasetClassification to update
|
* @param {keyof typeof DatasetClassifiers} classifier the property on the datasetClassification to update
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Component from '@ember/component';
|
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 ComputedProperty from '@ember/object/computed';
|
||||||
import { inject } from '@ember/service';
|
import { inject } from '@ember/service';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
@ -28,6 +28,10 @@ import {
|
|||||||
SuggestionIntent
|
SuggestionIntent
|
||||||
} from 'wherehows-web/constants';
|
} from 'wherehows-web/constants';
|
||||||
import { iterateArrayAsync } from 'wherehows-web/utils/array';
|
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
|
* Type alias for the response when container data items are batched
|
||||||
@ -52,7 +56,7 @@ type BatchContainerDataResult = Pick<
|
|||||||
| 'schemaless'
|
| 'schemaless'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const { successUpdating, failedUpdating } = compliancePolicyStrings;
|
const { successUpdating, failedUpdating, successUploading, invalidPolicyData } = compliancePolicyStrings;
|
||||||
|
|
||||||
export default class DatasetComplianceContainer extends Component {
|
export default class DatasetComplianceContainer extends Component {
|
||||||
/**
|
/**
|
||||||
@ -336,4 +340,43 @@ export default class DatasetComplianceContainer extends Component {
|
|||||||
onSuggestionsComplianceFeedback(uid: string | null = null, feedback: SuggestionIntent) {
|
onSuggestionsComplianceFeedback(uid: string | null = null, feedback: SuggestionIntent) {
|
||||||
saveDatasetComplianceSuggestionFeedbackByUrn(get(this, 'urn'), uid, feedback);
|
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?',
|
missingTypes: 'Looks like you may have forgotten to specify a `Field Format` for all ID fields?',
|
||||||
successUpdating: 'Changes have been successfully saved!',
|
successUpdating: 'Changes have been successfully saved!',
|
||||||
failedUpdating: 'An error occurred while saving.',
|
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.',
|
invalidPolicyData: 'Received policy in an unexpected format! Please check the provided attributes and try again.',
|
||||||
helpText: {
|
helpText: {
|
||||||
classification:
|
classification:
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
notifyOnChangeSetRequiresReview=(action "onCompliancePolicyChangeSetDrift")
|
notifyOnChangeSetRequiresReview=(action "onCompliancePolicyChangeSetDrift")
|
||||||
onSave=(action "savePrivacyCompliancePolicy")
|
onSave=(action "savePrivacyCompliancePolicy")
|
||||||
onReset=(action "resetPrivacyCompliancePolicy")
|
onReset=(action "resetPrivacyCompliancePolicy")
|
||||||
|
onComplianceUpload=(action "onComplianceUpload")
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{{/if}}
|
{{/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 };
|
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