adds typescript. adds mirage. updates dependencies. begins separating api concerns from app: datasets. removes jshint.

adds mirage model+factory for compliance suggestion

updates gitignore: removes typings. adds environment.d.ts declaration file for config/environment. adds unit test for datasets-test. adds extracted datasets api file

updates destroy app test helper

updates compliance component to work with modified api from compliance api: removes isSubject, adds securityClassification

updates uploaded compliance shape validation. updates the dataset route

adds typescript files to prettier linting

updates gitignore with vscode
This commit is contained in:
Seyi Adebajo 2017-08-15 23:16:38 -07:00
parent ab31c4706e
commit 4d0d746fe3
21 changed files with 1261 additions and 1178 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ logs/
dist
tmp
/commit
/.vscode/

View File

@ -1,4 +1,7 @@
module.exports = {
globals: {
server: true,
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 8,

View File

@ -11,8 +11,6 @@ npm-debug.log*
testem.log
yarn-error.log
# idea *.ts files
typings/
# vscode
jsconfig.json
/.vscode/

View File

@ -62,12 +62,6 @@ const datasetClassificationKey = 'complianceInfo.datasetClassification';
*/
const datasetClassifiersKeys = Object.keys(datasetClassifiers);
/**
* String constant identifying the classified fields on the security spec
* @type {String}
*/
const policyFieldClassificationKey = 'complianceInfo.fieldClassification';
/**
* A reference to the compliance policy entities on the complianceInfo map
* @type {string}
@ -347,15 +341,16 @@ export default Component.extend({
*/
mergeComplianceEntitiesAndColumnFields(columnIdFieldsToCurrentPrivacyPolicy = {}, truncatedColumnFields = []) {
return truncatedColumnFields.map(({ fieldName: identifierField, dataType }) => {
const { [identifierField]: { identifierType, isSubject, logicalType } } = columnIdFieldsToCurrentPrivacyPolicy;
const { [identifierField]: classification } = get(this, policyFieldClassificationKey) || {};
const {
[identifierField]: { identifierType, logicalType, securityClassification }
} = columnIdFieldsToCurrentPrivacyPolicy;
return {
identifierField,
dataType,
identifierType,
isSubject,
logicalType,
classification
classification: securityClassification
};
});
},
@ -381,7 +376,7 @@ export default Component.extend({
return columnFieldNames.reduce((acc, identifierField) => {
const currentPrivacyAttrs = getKeysOnField(
['identifierType', 'isSubject', 'logicalType'],
['identifierType', 'logicalType', 'securityClassification'],
identifierField,
complianceEntities
);
@ -483,9 +478,10 @@ export default Component.extend({
let defaultTypeClassification = defaultFieldDataTypeClassification[logicalType] || null;
// If the identifierType is of custom, set the default classification to the of a CUSTOM_ID, otherwise use value
// based on logicalType
defaultTypeClassification = identifierType === fieldIdentifierTypes.custom.value
? defaultFieldDataTypeClassification['CUSTOM_ID']
: defaultTypeClassification;
defaultTypeClassification =
identifierType === fieldIdentifierTypes.custom.value
? defaultFieldDataTypeClassification['CUSTOM_ID']
: defaultTypeClassification;
this.actions.onFieldClassificationChange.call(this, { identifierField }, { value: defaultTypeClassification });
},
@ -590,8 +586,7 @@ export default Component.extend({
onComplianceJsonUpload(textString) {
const policy = JSON.parse(textString);
if (isPolicyExpectedShape(policy)) {
const currentPolicy = get(this, 'complianceInfo');
set(this, 'complianceInfo', Object.assign({}, currentPolicy, policy));
set(this, 'complianceInfo', policy);
// If all is good, then we can saveCompliance so user does not have to manually click
return this.actions.saveCompliance();
@ -605,11 +600,7 @@ export default Component.extend({
*/
onComplianceDownloadJson() {
const currentPolicy = get(this, 'complianceInfo');
const policyProps = [
datasetClassificationKey,
policyFieldClassificationKey,
policyComplianceEntitiesKey
].map(name => name.split('.').pop());
const policyProps = [datasetClassificationKey, policyComplianceEntitiesKey].map(name => name.split('.').pop());
const policy = Object.assign({}, getProperties(currentPolicy, policyProps));
const href = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(policy))}`;
const download = `${get(this, 'datasetName')}_policy.json`;
@ -639,25 +630,19 @@ export default Component.extend({
* @param {String} logicalType
* @param {String} identifierType
*/
onFieldIdentifierTypeChange({ identifierField, logicalType }, { value: identifierType }) {
onFieldIdentifierTypeChange({ identifierField }, { value: identifierType }) {
const currentComplianceEntities = get(this, 'mergedComplianceEntitiesAndColumnFields');
// A reference to the current field in the compliance list if it exists
// A reference to the current field in the compliance list, it should exist even for empty complianceEntities
// since this is a reference created in the working copy: mergedComplianceEntitiesAndColumnFields
const currentFieldInComplianceList = currentComplianceEntities.findBy('identifierField', identifierField);
const subjectIdString = fieldIdentifierTypes.subjectMember.value;
// Some rendered identifierTypes may be masks of other underlying types, e.g. subjectId Member type is really
// a memberId with an attribute of isSubject in the affirmative
const unwrappedIdentifierType = subjectIdString === identifierType
? fieldIdentifierTypes.member.value
: identifierType;
setProperties(currentFieldInComplianceList, {
identifierType: unwrappedIdentifierType,
isSubject: subjectIdString === identifierType ? true : null,
identifierType,
logicalType: void 0
});
// Set the defaultClassification for the identifierField,
// although the classification is based on the logicalType,
// an identifierField may only have one valid logicalType for it's given identifierType
this.setDefaultClassification({ identifierField, identifierType: unwrappedIdentifierType });
this.setDefaultClassification({ identifierField, identifierType });
},
/**
@ -695,28 +680,11 @@ export default Component.extend({
'identifierField',
identifierField
);
let fieldClassification = get(this, policyFieldClassificationKey);
let updatedFieldClassification = {};
// For datasets initially without a fieldClassification, the default value is null
if (fieldClassification === null) {
fieldClassification = set(this, policyFieldClassificationKey, {});
}
// TODO:DSS-6719 refactor into mixin
this.clearMessages();
if (!classification && identifierField in fieldClassification) {
updatedFieldClassification = Object.assign(updatedFieldClassification, fieldClassification);
delete updatedFieldClassification[identifierField];
} else {
// fieldNames/identifierField can be paths i.e. identifierField.identifierPath.subPath
// using Ember.set trips up Ember and throws
updatedFieldClassification = Object.assign({}, fieldClassification, { [identifierField]: classification });
}
// Apply the updated classification value to the current instance of the field in working copy
set(currentFieldInComplianceList, 'classification', classification);
set(this, policyFieldClassificationKey, updatedFieldClassification);
},
/**

View File

@ -0,0 +1,15 @@
export default config;
/**
* Type declarations for
* import config from './config/environment'
*
* For now these need to be managed by the developer
* since different ember addons can materialize new entries.
*/
declare namespace config {
export const environment: any;
export const modulePrefix: string;
export const podModulePrefix: string;
export const locationType: string;
}

View File

@ -1,6 +1,6 @@
import Ember from 'ember';
import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/functions';
import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities';
import { datasetComplianceFor } from 'wherehows-web/utils/api';
const { Route, get, set, setProperties, isPresent, inject: { service }, $: { getJSON } } = Ember;
// TODO: DSS-6581 Create URL retrieval module
@ -19,7 +19,6 @@ const getDatasetReferencesUrl = id => `${datasetUrl(id)}/references`;
const getDatasetOwnersUrl = id => `${datasetUrl(id)}/owners`;
const getDatasetInstanceUrl = id => `${datasetUrl(id)}/instances`;
const getDatasetVersionUrl = (id, dbId) => `${datasetUrl(id)}/versions/db/${dbId}`;
const getDatasetPrivacyUrl = id => `${datasetUrl(id)}/privacy`;
let getDatasetColumn;
@ -122,19 +121,6 @@ export default Route.extend({
)
.forEach(func => func());
/**
* Fetches the current compliance policy for the rendered dataset
* @param {number} id the id of the dataset
* @return {Promise.<{complianceInfo: *, isNewComplianceInfo: boolean}>}
*/
const getComplianceInfo = async id => {
const response = await Promise.resolve(getJSON(getDatasetPrivacyUrl(id)));
const { msg, status, complianceInfo = createInitialComplianceInfo(id) } = response;
const isNewComplianceInfo = status === 'failed' && String(msg).includes('actual 0');
return { complianceInfo, isNewComplianceInfo };
};
/**
* Fetch the datasetColumn
* @param {number} id the id of the dataset
@ -183,8 +169,8 @@ export default Route.extend({
* @return {Promise.<void>}
*/
(async id => {
const [columns, privacy] = await Promise.all([getDatasetColumn(id), getComplianceInfo(id)]);
const { complianceInfo, isNewComplianceInfo } = privacy;
const [columns, compliance] = await Promise.all([getDatasetColumn(id), datasetComplianceFor(id)]);
const { complianceInfo, isNewComplianceInfo } = compliance;
setProperties(controller, {
complianceInfo,
isNewComplianceInfo,

View File

@ -0,0 +1,27 @@
/**
* Defines the available interface on a Mirage server.
* This is not an exhaustive list but exposes some of the api that's used within the app
*/
export interface IMirageServer {
options: object;
urlPrefix: string;
namespace: string;
timing: number;
logging: boolean;
pretender: object;
environment: string;
get: (this: IMirageServer, path: string, ...args: Array<any>) => void;
post: (this: IMirageServer, path: string, ...args: Array<any>) => void;
put: (this: IMirageServer, path: string, ...args: Array<any>) => void;
delete: (this: IMirageServer, path: string, ...args: Array<any>) => void;
del: (this: IMirageServer, path: string, ...args: Array<any>) => void;
patch: (this: IMirageServer, path: string, ...args: Array<any>) => void;
head: (this: IMirageServer, path: string, ...args: Array<any>) => void;
isTest: (this: IMirageServer) => boolean;
passthrough: (...paths: Array<string>) => void;
loadFixtures: (...files: Array<string>) => void;
loadFactories: (factoryMap: object) => void;
create: (type: string, options?: object) => object;
createList: <T>(type: string, amount: number, traitsAndOverrides?: object) => Array<T>;
shutdown: (this: IMirageServer) => void;
}

View File

@ -0,0 +1 @@
declare module '*';

View File

@ -0,0 +1,55 @@
import Ember from 'ember';
/// <reference path='wherehows-web/typings/untyped-js-module' />
import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/functions';
import { apiRoot } from 'wherehows-web/utils/api';
const { $: { getJSON }, assert } = Ember;
/**
* Defines the endpoint for datasets
* @type {string}
*/
const datasetsUrlRoot = `${apiRoot}/datasets`;
/**
* Constructs a url to get a dataset with a given id
* @param {number} id the id of the dataset
* @return {string} the dataset url
*/
const datasetUrlById = (id: number): string => `${datasetsUrlRoot}/${id}`;
/**
* Constructs the dataset compliance url
* @param {number} id the id of the dataset
* @return {string} the dataset compliance url
*/
const datasetComplianceUrlById = (id: number): string => `${datasetUrlById(id)}/compliance`;
/**
* Fetches the current compliance policy for a dataset with thi given id
* @param {number} id the id of the dataset
* @return {Promise<{isNewComplianceInfo: boolean, complianceInfo: *}>}
*/
const datasetComplianceFor = async (id: number): Promise<{ isNewComplianceInfo: boolean; complianceInfo: any }> => {
assert(`Expected id to be a number but received ${typeof id}`, typeof id === 'number');
const failedStatus = 'failed';
const notFound = 'actual 0';
// complianceInfo contains the compliance data for the specified dataset
let {
msg = '',
status,
complianceInfo
}: { msg: string; status: string; complianceInfo: any } = await Promise.resolve(
getJSON(datasetComplianceUrlById(id))
);
// If the endpoint responds with a failed status, and the msg contains the indicator that a compliance does not exist
const isNewComplianceInfo: boolean = status === failedStatus && String(msg).includes(notFound);
if (isNewComplianceInfo) {
complianceInfo = createInitialComplianceInfo(id);
}
return { isNewComplianceInfo, complianceInfo };
};
export { datasetsUrlRoot, datasetComplianceFor };

View File

@ -0,0 +1,12 @@
/**
* Defines the root path for wherehows front-end api requests
* @type {string}
*/
const apiRoot = '/api/v1';
export * from 'wherehows-web/utils/api/datasets';
export {
apiRoot
}

View File

@ -1,5 +1,8 @@
import Ember from 'ember';
import { datasetClassifiers } from 'wherehows-web/constants/dataset-classification';
const { assert } = Ember;
/**
* Builds a default shape for securitySpecification & privacyCompliancePolicy with default / unset values
* for non null properties as per Avro schema
@ -29,13 +32,12 @@ const policyShape = {
keys: [
'identifierField:string',
'identifierType:string',
'isSubject:boolean|object',
'securityClassification:string',
'logicalType:string|object|undefined'
]
}
},
datasetClassification: { type: 'object', keys: Object.keys(datasetClassifiers).map(key => `${key}:boolean`) },
fieldClassification: { type: 'object' }
datasetClassification: { type: 'object', keys: Object.keys(datasetClassifiers).map(key => `${key}:boolean`) }
};
/**
@ -45,20 +47,25 @@ const policyShape = {
*/
const isPolicyExpectedShape = (candidatePolicy = {}) => {
const candidateMatchesShape = policyKey => {
assert(
`Expected each compliance policy attribute to be one of ${Object.keys(policyShape)}, but got ${policyKey}`,
policyShape.hasOwnProperty(policyKey)
);
const policyProps = policyShape[policyKey];
const expectedType = policyProps.type;
const policyKeyValue = candidatePolicy[policyKey];
const isValueExpectedType = expectedType === 'array'
? Array.isArray(policyKeyValue)
: typeof policyKeyValue === expectedType;
const typeDeclarations = {
get array() {
return policyProps.of.keys;
},
get object() {
return policyProps.keys;
}
}[expectedType] || [];
const isValueExpectedType =
expectedType === 'array' ? Array.isArray(policyKeyValue) : typeof policyKeyValue === expectedType;
const typeDeclarations =
{
get array() {
return policyProps.of.keys;
},
get object() {
return policyProps.keys;
}
}[expectedType] || [];
if (!policyKeyValue || !isValueExpectedType) {
return false;

View File

@ -0,0 +1,9 @@
import { IMirageServer } from 'wherehows-web/typings/ember-cli-mirage';
export default function(this: IMirageServer) {
this.namespace = '/api/v1/';
this.get('/compliance/suggestions', () => {});
this.passthrough();
}

View File

@ -0,0 +1,16 @@
import { Factory, faker } from 'ember-cli-mirage';
import { fieldIdentifierTypes, idLogicalTypes } from 'wherehows-web/constants';
const identifierTypeValues = Object.keys(fieldIdentifierTypes);
export default Factory.extend({
fieldName: 'memberUrn',
identifierTypePrediction: {
value: () => faker.list.random(...identifierTypeValues),
confidence: 1
},
logicalTypePrediction: {
value: faker.list.random(...idLogicalTypes),
confidence: 1
}
});

View File

@ -0,0 +1,4 @@
import { Model } from 'ember-cli-mirage';
export default Model.extend({
});

View File

@ -0,0 +1,9 @@
export default function(/* server */) {
/*
Seed your development database using your factories.
This data will not be loaded in your tests.
*/
// server.createList('post', 10);
}

View File

@ -0,0 +1,4 @@
import { JSONAPISerializer } from 'ember-cli-mirage';
export default JSONAPISerializer.extend({
});

View File

@ -16,46 +16,50 @@
"precommit": "lint-staged"
},
"devDependencies": {
"bower": "1.8.0",
"broccoli-asset-rev": "2.5.0",
"broccoli-funnel": "1.1.0",
"broccoli-merge-trees": "1.2.1",
"ember-ajax": "2.5.5",
"ember-cli": "2.11.1",
"ember-cli-app-version": "2.0.1",
"ember-cli-babel": "5.2.4",
"ember-cli-bootstrap-sassy": "0.5.5",
"ember-cli-dependency-checker": "1.3.0",
"ember-cli-eyeglass": "1.3.0",
"ember-cli-htmlbars": "1.1.1",
"ember-cli-inject-live-reload": "1.6.1",
"ember-cli-jshint": "2.0.1",
"ember-cli-moment-shim": "3.0.1",
"ember-cli-qunit": "3.1.1",
"ember-cli-release": "0.2.9",
"ember-cli-sri": "2.1.1",
"ember-cli-test-loader": "1.1.1",
"ember-cli-uglify": "1.2.0",
"ember-composable-helpers": "2.0.0",
"ember-data": "2.11.1",
"ember-export-application-global": "1.1.1",
"ember-load-initializers": "0.5.1",
"ember-lodash-shim": "2.0.2",
"ember-metrics": "0.10.0",
"ember-moment": "7.3.0",
"ember-pikaday": "2.2.1",
"ember-redux-shim": "1.1.1",
"ember-redux-thunk-shim": "1.1.2",
"ember-resolver": "2.1.1",
"ember-symbol-observable": "0.1.2",
"eyeglass": "1.2.1",
"eyeglass-restyle": "1.0.19",
"@types/ember": "^2.7.44",
"@types/ember-testing-helpers": "0.0.1",
"@types/rsvp": "^3.3.1",
"bower": "^1.8.0",
"broccoli-asset-rev": "^2.5.0",
"broccoli-funnel": "^1.1.0",
"broccoli-merge-trees": "^1.2.1",
"ember-ajax": "^2.5.5",
"ember-cli": "^2.11.1",
"ember-cli-app-version": "^2.0.1",
"ember-cli-babel": "^5.2.4",
"ember-cli-bootstrap-sassy": "^0.5.5",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eyeglass": "^1.3.0",
"ember-cli-htmlbars": "^1.1.1",
"ember-cli-inject-live-reload": "^1.6.1",
"ember-cli-moment-shim": "^3.0.1",
"ember-cli-qunit": "^3.1.1",
"ember-cli-release": "^0.2.9",
"ember-cli-sri": "^2.1.1",
"ember-cli-test-loader": "^1.1.1",
"ember-cli-typescript": "^1.0.0",
"ember-cli-uglify": "^1.2.0",
"ember-composable-helpers": "^2.0.0",
"ember-data": "^2.11.1",
"ember-export-application-global": "^1.1.1",
"ember-load-initializers": "^0.5.1",
"ember-lodash-shim": "^2.0.2",
"ember-metrics": "^0.10.0",
"ember-moment": "^7.3.0",
"ember-pikaday": "^2.2.1",
"ember-redux-shim": "^1.1.1",
"ember-redux-thunk-shim": "^1.1.2",
"ember-resolver": "^2.1.1",
"ember-symbol-observable": "^0.1.2",
"eyeglass": "^1.2.1",
"eyeglass-restyle": "^1.0.19",
"husky": "^0.13.3",
"lint-staged": "^3.4.2",
"loader.js": "4.1.0",
"node-sass": "^4.5.2",
"redux": "^3.6.0",
"redux-thunk": "^2.2.0"
"redux-thunk": "^2.2.0",
"typescript": "^2.4.2"
},
"engines": {
"node": ">= 0.12.0"
@ -64,6 +68,7 @@
"dependencies": {
"dynamic-link": "0.2.1",
"ember-aupac-typeahead": "2.1.2",
"ember-cli-mirage": "0.3.4",
"ember-cli-string-helpers": "1.0.0",
"ember-lodash": "4.17.2",
"ember-network": "0.3.1",
@ -79,7 +84,7 @@
"lint-staged": {
"gitDir": "../",
"linters": {
"wherehows-web/{app,tests}/**/*.js": [
"wherehows-web/{app,tests}/**/*.{ts,js}": [
"prettier --print-width 120 --single-quote --write",
"git add"
]

View File

@ -2,4 +2,7 @@ import Ember from 'ember';
export default function destroyApp(application) {
Ember.run(application, 'destroy');
if (window.server) {
window.server.shutdown();
}
}

View File

@ -0,0 +1,11 @@
import datasetComplianceFor from 'wherehows-web/utils/api/datasets';
import { module, test } from 'qunit';
module('Unit | Utility | api/datasets');
test('it has expected functions', function(assert) {
assert.expect(1);
const result = datasetComplianceFor(0);
assert.ok(result instanceof Promise, 'datasetComplianceFor is an async function');
});

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2015",
"moduleResolution": "node",
"noEmitOnError": true,
"baseUrl": ".",
"sourceMap": true,
"noEmit": true,
"strict": true,
"pretty": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"paths": {
"wherehows-web/*": ["app/*"],
"wherehows-web/tests/*": ["tests/*"]
}
},
"include": ["app/**/*", "tests/**/*", "mirage/**/*"],
"exclude": ["tmp", "dist", "node_modules", "bower_components", "vendor", ".git", ".gradle", ".idea", "logs"]
}

File diff suppressed because it is too large Load Diff