import { IOwner, IOwnerPostResponse, IOwnerResponse, IOwnerTypeResponse } from 'wherehows-web/typings/api/datasets/owners'; import { IPartyEntity, IPartyEntityResponse, IPartyProps, IUserEntityMap } from 'wherehows-web/typings/api/datasets/party-entities'; import { notFoundApiError } from 'wherehows-web/utils/api'; import { datasetUrlById, datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared'; import { getJSON, postJSON } from 'wherehows-web/utils/api/fetcher'; import { getApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared'; import { arrayFilter, arrayMap } from 'wherehows-web/utils/array'; import { fleece } from 'wherehows-web/utils/object'; /** * Defines a string enum for valid owner types */ enum OwnerIdType { User = 'USER', Group = 'GROUP' } /** * Defines the string enum for the OwnerType attribute * @type {string} */ enum OwnerType { Owner = 'Owner', Consumer = 'Consumer', Delegate = 'Delegate', Producer = 'Producer', Stakeholder = 'Stakeholder' } /** * Accepted string values for the namespace of a user */ enum OwnerUrnNamespace { corpUser = 'urn:li:corpuser', groupUser = 'urn:li:corpGroup', multiProduct = 'urn:li:multiProduct' } enum OwnerSource { Scm = 'SCM', Nuage = 'NUAGE', Sos = 'SOS', Db = 'DB', Audit = 'AUDIT', Jira = 'JIRA', RB = 'RB', Ui = 'UI', Fs = 'FS', Other = 'OTHER' } /** * Constructs the dataset owners url * @param {number} id the id of the dataset * @return {string} the dataset owners url */ const datasetOwnersUrlById = (id: number): string => `${datasetUrlById(id)}/owners`; /** * Returns the dataset owners url by urn * @param {string} urn * @return {string} */ const datasetOwnersUrlByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/owners`; /** * Returns the party entities url * @type {string} */ const partyEntitiesUrl = `${getApiRoot()}/party/entities`; /** * Returns the owner types url * @return {string} */ const datasetOwnerTypesUrl = () => `${getApiRoot()}/owner/types`; /** * Requests the list of dataset owners from the GET endpoint, converts the modifiedTime property * to a date object * @param {number} id the dataset Id * @return {Promise>} the current list of dataset owners */ const readDatasetOwners = async (id: number): Promise> => { const { owners = [], status, msg } = await getJSON({ url: datasetOwnersUrlById(id) }); if (status === ApiStatus.OK) { return ownersWithModifiedTimeAsDate(owners); } throw new Error(msg); }; /** * Modifies an owner object by applying the modified date property as a Date object * @param {IOwner} owner * @return {IOwner} */ const ownerWithModifiedTimeAsDate = (owner: IOwner): IOwner => ({ ...owner, modifiedTime: new Date(owner.modifiedTime) }); // Api response is always in number format /** * Modifies a list of owners with a modified date property as a Date object * @type {(array: Array) => Array} */ const ownersWithModifiedTimeAsDate = arrayMap(ownerWithModifiedTimeAsDate); /** * Reads the owners for dataset by urn * @param {string} urn * @return {Promise>} */ const readDatasetOwnersByUrn = async (urn: string): Promise> => { let owners: Array = []; try { ({ owners = [] } = await getJSON>({ url: datasetOwnersUrlByUrn(urn) })); return ownersWithModifiedTimeAsDate(owners); } catch (e) { if (notFoundApiError(e)) { return owners; } else { throw e; } } }; /** * Persists the updated list of dataset owners * @param {number} id the id of the dataset * @param {string} csrfToken * @param {Array} updatedOwners the updated list of owners for this dataset * @return {Promise} */ const updateDatasetOwners = async ( id: number, csrfToken: string, updatedOwners: Array ): Promise => { const { status, msg } = await postJSON({ url: datasetOwnersUrlById(id), headers: { 'csrf-token': csrfToken }, data: { csrfToken, owners: updatedOwners } }); if ([ApiStatus.OK, ApiStatus.SUCCESS].includes(status)) { return { status: ApiStatus.OK }; } throw new Error(msg); }; /** * Updates the owners on a dataset by urn * @param {string} urn * @param {string} csrfToken * @param {Array} updatedOwners * @return {Promise} */ const updateDatasetOwnersByUrn = (urn: string, csrfToken: string = '', updatedOwners: Array): Promise<{}> => { const ownersWithoutModifiedTime = arrayMap(fleece(['modifiedTime'])); return postJSON<{}>({ url: datasetOwnersUrlByUrn(urn), headers: { 'csrf-token': csrfToken }, data: { csrfToken, owners: ownersWithoutModifiedTime(updatedOwners) // strips the modified time from each owner, remote is source of truth } }); }; /** * Reads the owner types list on a dataset * @return {Promise>} */ const readDatasetOwnerTypes = async (): Promise> => { const url = datasetOwnerTypesUrl(); const { ownerTypes = [] } = await getJSON({ url }); return ownerTypes.sort((a: string, b: string) => a.localeCompare(b)); }; /** * Determines if an owner type supplied is not of type consumer * @param {OwnerType} ownerType * @return {boolean} */ const isNotAConsumer = (ownerType: OwnerType): boolean => ownerType !== OwnerType.Consumer; /** * Reads the dataset owner types and filters out the OwnerType.Consumer type from the list * @return {Promise>} */ const readDatasetOwnerTypesWithoutConsumer = async () => arrayFilter(isNotAConsumer)(await readDatasetOwnerTypes()); /** * Requests party entities and if the response status is OK, resolves with an array of entities * @return {Promise>} */ const readPartyEntities = async (): Promise> => { const { status, userEntities = [], msg } = await getJSON({ url: partyEntitiesUrl }); return status === ApiStatus.OK ? userEntities : Promise.reject(msg); }; /** * 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} */ const getUserEntities: () => Promise = (() => { /** * 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 } = { result: null, userEntitiesSource: [] }; let inflightRequest: Promise>; /** * Invokes the requestor for party entities, and adds perf optimizations listed above * @return {Promise} */ return async (): Promise => { // 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 = readPartyEntities(); } const userEntities: Array = await inflightRequest; return (cache.result = { userEntities, userEntitiesMaps: readPartyEntitiesMap(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 * @param {Array} partyEntities * @return {IUserEntityMap} */ const readPartyEntitiesMap = (partyEntities: Array): IUserEntityMap => partyEntities.reduce( (map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map), {} ); export { readDatasetOwners, readDatasetOwnersByUrn, updateDatasetOwnersByUrn, readDatasetOwnerTypesWithoutConsumer, readPartyEntities, readPartyEntitiesMap, getUserEntities, updateDatasetOwners, OwnerIdType, OwnerType, OwnerUrnNamespace, OwnerSource };