Merge pull request #704 from theseyi/ownership

ts all the things. refactor getuserentities: cache and allow only one…
This commit is contained in:
Seyi Adebajo 2017-08-24 14:18:20 -07:00 committed by GitHub
commit e20a5c84cb
6 changed files with 133 additions and 85 deletions

View File

@ -174,6 +174,20 @@ export default Component.extend({
updatedOwner, updatedOwner,
...updatingOwners.slice(ownerPosition + 1) ...updatingOwners.slice(ownerPosition + 1)
]; ];
// The list of ldap userNames currently in the list
const userNames = updatedOwners.mapBy('userName');
// Checks that the userNames are not already in the list of current owners
const hasDuplicates = new Set(userNames).size !== userNames.length;
if (hasDuplicates) {
set(
this,
'errorMessage',
'Uh oh! Looks like there are duplicates in the list of owners, please remove them first.'
);
return;
}
// Full reset of the `owners` list with the new list // Full reset of the `owners` list with the new list
sourceOwners.setObjects(updatedOwners); sourceOwners.setObjects(updatedOwners);
@ -213,6 +227,7 @@ export default Component.extend({
* @return {Promise.<void>} * @return {Promise.<void>}
*/ */
async editUserName(currentOwner, userName) { async editUserName(currentOwner, userName) {
set(this, 'errorMessage', '');
if (userName) { if (userName) {
// getUser returns a promise, treat as such // getUser returns a promise, treat as such
const getUser = get(this, 'ldapUsers.getPartyEntityWithUserName'); const getUser = get(this, 'ldapUsers.getPartyEntityWithUserName');

View File

@ -3,9 +3,8 @@ import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities';
import { datasetComplianceFor, datasetComplianceSuggestionsFor } from 'wherehows-web/utils/api/datasets/compliance'; import { datasetComplianceFor, datasetComplianceSuggestionsFor } from 'wherehows-web/utils/api/datasets/compliance';
import { import {
getDatasetOwners, getDatasetOwners,
getPartyEntities, getUserEntities,
isRequiredMinOwnersNotConfirmed, isRequiredMinOwnersNotConfirmed
getPartyEntitiesMap
} from 'wherehows-web/utils/api/datasets/owners'; } from 'wherehows-web/utils/api/datasets/owners';
const { Route, get, set, setProperties, isPresent, inject: { service }, $: { getJSON } } = Ember; const { Route, get, set, setProperties, isPresent, inject: { service }, $: { getJSON } } = Ember;
@ -395,15 +394,15 @@ export default Route.extend({
// Retrieve the current owners of the dataset and store on the controller // Retrieve the current owners of the dataset and store on the controller
(async id => { (async id => {
const [owners, userEntities] = await Promise.all([getDatasetOwners(id), getPartyEntities()]); const [owners, { userEntitiesSource, userEntitiesMaps }] = await Promise.all([
getDatasetOwners(id),
getUserEntities()
]);
setProperties(controller, { setProperties(controller, {
requiredMinNotConfirmed: isRequiredMinOwnersNotConfirmed(owners), requiredMinNotConfirmed: isRequiredMinOwnersNotConfirmed(owners),
owners, owners,
userEntitiesMaps: getPartyEntitiesMap(userEntities), userEntitiesMaps,
get userEntitiesSource() { userEntitiesSource
// TODO: memoize
return Object.keys(this.userEntitiesMaps);
}
}); });
})(id); })(id);
}, },

View File

@ -1,74 +0,0 @@
import Ember from 'ember';
const { isEmpty, Service, $: { getJSON } } = Ember;
const partyEntitiesUrl = '/api/v1/party/entities';
const cache = {
// Cache containing results from the last request to partyEntities api
partyEntities: null
};
/**
* Async request for partyEntities. Caches the value from the initial request
* @return {Promise.<Object|void>}
*/
const getLDAPUsers = () =>
new Promise(resolve => {
const cachedResults = cache.partyEntities;
// Resolve with cachedResults if this has been previously requested
if (!isEmpty(cachedResults)) {
return resolve(cachedResults);
}
// Cast $.getJSON to native Promise
Promise.resolve(getJSON(partyEntitiesUrl))
.then(({ status, userEntities = [] }) => {
if (status === 'ok' && userEntities.length) {
/**
* @type {Object} userEntitiesMaps hash of userEntities: label -> displayName
*/
const userEntitiesMaps = userEntities.reduce(
(map, { label, displayName }) => ((map[label] = displayName), map),
{}
);
return {
userEntities,
userEntitiesMaps,
userEntitiesSource: Object.keys(userEntitiesMaps)
};
}
})
.then(results => (cache.partyEntities = results))
.then(resolve);
});
/**
* Takes a userNameQuery query and find userNames that match by starting with
* the pattern
* @param {String} userNameQuery pattern to search for
* @param {Function} syncResults callback
* @param {Function} asyncResults callback
*/
const ldapResolver = (userNameQuery, syncResults, asyncResults) => {
const regex = new RegExp(`^${userNameQuery}.*`, 'i');
getLDAPUsers()
.then(({ userEntitiesSource = {} }) => userEntitiesSource.filter(entity => regex.test(entity)))
.then(asyncResults);
};
/**
* For a given userName, find the userEntity object that contains the userName
* @param {String} userName the unique userName
* @return {Promise.<TResult|null>} resolves with the userEntity or null otherwise
*/
const getPartyEntityWithUserName = userName =>
getLDAPUsers().then(({ userEntities }) => userEntities.find(({ label }) => label === userName) || null);
export default Service.extend({
getPartyEntityWithUserName,
userNamesResolver: ldapResolver,
fetchUserNames: getLDAPUsers
});

View File

@ -0,0 +1,35 @@
import Ember from 'ember';
import { getUserEntities } from 'wherehows-web/utils/api/datasets/owners';
import { IPartyEntity, IPartyProps } from 'wherehows-web/typings/api/datasets/party-entities';
const { Service } = Ember;
/**
* Takes a userNameQuery query and find userNames that match by starting with
* the pattern
* @param {string} userNameQuery pattern to search for
* @param {Function} _syncResults callback
* @param {Function} asyncResults callback
* @return {Promise<void>}
*/
const ldapResolver = async (userNameQuery: string, _syncResults: Function, asyncResults: Function): Promise<void> => {
const ldapRegex = new RegExp(`^${userNameQuery}.*`, 'i');
const { userEntitiesSource = [] }: IPartyProps = await getUserEntities();
asyncResults(userEntitiesSource.filter((entity: string) => ldapRegex.test(entity)));
};
/**
* For a given userName, find the userEntity object that contains the userName
* @param {string} userName the unique userName
* @return {Promise<IPartyEntity>} resolves with the userEntity or null otherwise
*/
const getPartyEntityWithUserName = (userName: string): Promise<IPartyEntity | null> =>
getUserEntities().then(
({ userEntities }: IPartyProps) => userEntities.find(({ label }: { label: string }) => label === userName) || null
);
export default Service.extend({
getPartyEntityWithUserName,
userNamesResolver: ldapResolver,
fetchUserNames: getUserEntities
});

View File

@ -16,3 +16,19 @@ export interface IPartyEntityResponse {
status: ApiStatus; status: ApiStatus;
userEntities?: Array<IPartyEntity>; userEntities?: Array<IPartyEntity>;
} }
/**
* Describes a userEntityMap interface
*/
export interface userEntityMap {
[label: string]: string;
}
/**
* Describes the props resolved by the getUserEntities function
*/
export interface IPartyProps {
userEntities: Array<IPartyEntity>;
userEntitiesMaps: userEntityMap;
userEntitiesSource: Array<keyof userEntityMap>;
}

View File

@ -1,7 +1,12 @@
import Ember from 'ember'; import Ember from 'ember';
import { ApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared'; import { ApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared';
import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared';
import { IPartyEntity, IPartyEntityResponse } from 'wherehows-web/typings/api/datasets/party-entities'; import {
IPartyEntity,
IPartyEntityResponse,
IPartyProps,
userEntityMap
} from 'wherehows-web/typings/api/datasets/party-entities';
import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners'; import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners';
/** /**
@ -48,12 +53,64 @@ export const getPartyEntities = async (): Promise<Array<IPartyEntity>> => {
return status === ApiStatus.OK ? userEntities : Promise.reject(status); return status === ApiStatus.OK ? userEntities : Promise.reject(status);
}; };
/**
* IIFE prepares the environment scope and returns a closure function that ensures that
* there is ever only one inflight request for userEntities.
* Resolves all subsequent calls with the result for the initial invocation.
* userEntitiesSource property is also lazy evaluated and cached for app lifetime.
* @type {() => Promise<IPartyProps>}
*/
export const getUserEntities: () => Promise<IPartyProps> = (() => {
/**
* Memoized reference to the resolved value of a previous invocation to curried function in getUserEntities
* @type {{result: IPartyProps | null}}
*/
const cache: { result: IPartyProps | null; userEntitiesSource: Array<keyof userEntityMap> } = {
result: null,
userEntitiesSource: []
};
let inflightRequest: Promise<Array<IPartyEntity>>;
/**
* Invokes the requestor for party entities, and adds perf optimizations listed above
* @return {Promise<IPartyProps>}
*/
return async (): Promise<IPartyProps> => {
// If a previous request has already resolved, return the cached value
if (cache.result) {
return cache.result;
}
// If we don't already have a previous api request for party entities,
// assign a new one to free variable
if (!inflightRequest) {
inflightRequest = getPartyEntities();
}
const userEntities: Array<IPartyEntity> = await inflightRequest;
return (cache.result = {
userEntities,
userEntitiesMaps: getPartyEntitiesMap(userEntities),
// userEntitiesSource is not usually needed immediately
// hence using a getter for lazy evaluation
get userEntitiesSource() {
const userEntitiesSource = cache.userEntitiesSource;
if (userEntitiesSource.length) {
return userEntitiesSource;
}
return (cache.userEntitiesSource = Object.keys(this.userEntitiesMaps));
}
});
};
})();
/** /**
* Transforms a list of party entities into a map of entity label to displayName value * Transforms a list of party entities into a map of entity label to displayName value
* @param {Array<IPartyEntity>} partyEntities * @param {Array<IPartyEntity>} partyEntities
* @return {Object<string>} * @return {Object<string>}
*/ */
export const getPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): { [label: string]: string } => export const getPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): userEntityMap =>
partyEntities.reduce( partyEntities.reduce(
(map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map), (map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map),
{} {}