mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-05 15:57:58 +00:00
424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
import { IEntityRenderProps } from '@datahub/data-models/types/entity/rendering/entity-render-props';
|
|
import { Snapshot } from '@datahub/metadata-types/types/metadata/snapshot';
|
|
import { computed } from '@ember/object';
|
|
import { MetadataAspect } from '@datahub/metadata-types/types/metadata/aspect';
|
|
import { getMetadataAspect } from '@datahub/metadata-types/constants/metadata/aspect';
|
|
import { IOwner } from '@datahub/metadata-types/types/common/owner';
|
|
import { map } from '@ember/object/computed';
|
|
import {
|
|
IEntityLinkAttrs,
|
|
EntityLinkNode,
|
|
IBrowsePath,
|
|
IEntityLinkAttrsWithCount,
|
|
AppRoute
|
|
} from '@datahub/data-models/types/entity/shared';
|
|
import { NotImplementedError } from '@datahub/data-models/constants/entity/shared/index';
|
|
import { readBrowse, readBrowsePath } from '@datahub/data-models/api/browse';
|
|
import { getFacetDefaultValueForEntity } from '@datahub/data-models/entity/utils/facets';
|
|
import { InstitutionalMemory } from '@datahub/data-models/models/aspects/institutional-memory';
|
|
import { IBaseEntity } from '@datahub/metadata-types/types/entity';
|
|
import { readEntity } from '@datahub/data-models/api/entity';
|
|
|
|
/**
|
|
* Interfaces and abstract classes define the "instance side" of a type / class,
|
|
* therefore ts will only check the instance side
|
|
* To check the static side / constructor, the interface should define the constructor, then static properties
|
|
*
|
|
* statics is a generic ClassDecorator that checks the static side T of the decorated class
|
|
* @template T {T extends new (...args: Array<any>) => void} constrains T to constructor interfaces
|
|
* @type {() => ClassDecorator}
|
|
*/
|
|
export const statics = <T extends new (...args: Array<unknown>) => void>(): ((c: T) => void) => (_ctor: T): void => {};
|
|
|
|
/**
|
|
* Defines the interface for the static side or constructor of a class that extends BaseEntity<T>
|
|
* @export
|
|
* @interface IBaseEntityStatics
|
|
* @template T constrained by the IBaseEntity interface, the entity interface that BaseEntity subclass will encapsulate
|
|
*/
|
|
export interface IBaseEntityStatics<T> {
|
|
new (urn: string): BaseEntity<T>;
|
|
|
|
/**
|
|
* Properties that guide the rendering of ui elements and features in the host application
|
|
* @readonly
|
|
* @static
|
|
*/
|
|
renderProps: IEntityRenderProps;
|
|
|
|
/**
|
|
* Statically accessible name of the concrete DataModel type
|
|
* @type {string}
|
|
*/
|
|
displayName: string;
|
|
|
|
/**
|
|
* Queries the entity's endpoint to retrieve the list of nodes that are contained in the hierarchy
|
|
* @param {(Array<string>)} args list of string values corresponding to the different hierarchical categories for the entity
|
|
*/
|
|
readCategories(...args: Array<string>): Promise<IBrowsePath>;
|
|
|
|
/**
|
|
* Queries the entity's endpoint to get the count for categories. This call will be made if 'showCount' is true and
|
|
* only will be available in the card layout.
|
|
* @param args list fo segments in the entity hierarchy, maybe be culled from the entity, entity urn, or query (user entered url)
|
|
*/
|
|
// TODO META-8863 remove once dataset is migrated
|
|
readCategoriesCount(...segments: Array<string>): Promise<number>;
|
|
|
|
/**
|
|
* Queries the batch GET endpoint for snapshots for the supplied urns
|
|
*/
|
|
readSnapshots(_urns: Array<string>): Promise<Array<Snapshot>>;
|
|
|
|
/**
|
|
* Builds a search query keyword from a list of segments for the related DataModelEntity
|
|
* @memberof IBaseEntityStatics
|
|
*/
|
|
getQueryForHierarchySegments(_segments: Array<string>): string;
|
|
|
|
/**
|
|
* Gets the entity link for the current entity
|
|
*/
|
|
getLinkForEntity(params: { entityUrn: string; displayName: string }): IEntityLinkAttrs | void;
|
|
}
|
|
|
|
/**
|
|
* Check if entity extends baseEntity by checking on the urn property
|
|
*/
|
|
export const isBaseEntity = <T extends {}>(entity?: T | IBaseEntity): entity is IBaseEntity =>
|
|
(entity && entity.hasOwnProperty('urn')) || false;
|
|
|
|
/**
|
|
* This defines the base attributes and methods for the instance side of an entity data model
|
|
* i.e. the public interface for an entity object
|
|
* Shared methods and default properties may be defined on this class
|
|
* Properties intended to be implemented by the concrete entity, and shared methods that
|
|
* need to be delegated to the concrete entity should be declared here
|
|
* @export
|
|
* @abstract
|
|
* @class BaseEntity
|
|
* @template T the entity interface that the entity model (subclass) encapsulates
|
|
*/
|
|
export abstract class BaseEntity<T extends {} | IBaseEntity> {
|
|
/**
|
|
* A reference to the derived concrete entity instance
|
|
* @type {T}
|
|
*/
|
|
entity?: T;
|
|
|
|
/**
|
|
* References the Snapshot for the related Entity
|
|
* @type {Snapshot}
|
|
*/
|
|
snapshot?: Snapshot;
|
|
|
|
/**
|
|
* References the wiki related documents and objects related to this entity
|
|
*/
|
|
institutionalMemories?: Array<InstitutionalMemory>;
|
|
|
|
/**
|
|
* Hook for custom fetching operations after entity is created
|
|
*/
|
|
onAfterCreate(): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* A dictionary of host Ember application routes which can be used as route arguments to the link-to helper
|
|
*/
|
|
get hostRoutes(): Record<string, AppRoute | void> {
|
|
return { dataSourceRoute: 'datasets.dataset' };
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the entity has been removed (soft deletion), reads from reified entity,
|
|
* otherwise defaults to false
|
|
* @readonly
|
|
* @type {boolean}
|
|
* @memberof BaseEntity
|
|
*/
|
|
@computed('entity')
|
|
get removed(): boolean {
|
|
return isBaseEntity<T>(this.entity) ? this.entity.removed : false;
|
|
}
|
|
|
|
/**
|
|
* Selects the list of owners from the ownership aspect attribute of the entity
|
|
* All entities have an ownership aspect have this property exists on the base and is inherited
|
|
*
|
|
* This provides the full IOwnership objects in a list, if what is needed is just the list of
|
|
* owner urns, the macro value ownerUrns provides that immediately
|
|
* @readonly
|
|
* @type {Array<IOwner>}
|
|
* @memberof BaseEntity
|
|
*/
|
|
@computed('snapshot')
|
|
get owners(): Array<IOwner> {
|
|
const ownership = getMetadataAspect(this.snapshot)(
|
|
'com.linkedin.common.Ownership'
|
|
) as MetadataAspect['com.linkedin.common.Ownership'];
|
|
|
|
return ownership ? ownership.owners : [];
|
|
}
|
|
|
|
/**
|
|
* Extracts the owner urns into a list from each IOwner instance
|
|
* @readonly
|
|
* @type {Array<string>}
|
|
* @memberof BaseEntity
|
|
*/
|
|
@map('owners.[]', ({ owner }: IOwner): string => owner)
|
|
ownerUrns!: Array<string>;
|
|
|
|
/**
|
|
* Class instance JSON serialization does not by default serialize non-enumerable property names
|
|
* This provides a custom toJSON method to ensure that displayName attribute is present when serialized,
|
|
* allowing correct de-serialization back to source instance / DataModelEntity type
|
|
*/
|
|
toJSON(): this {
|
|
return { ...this, displayName: this.displayName };
|
|
}
|
|
|
|
/**
|
|
* Statically accessible Base entity kind discriminant
|
|
* @static
|
|
*/
|
|
static kind = 'BaseEntity';
|
|
|
|
/**
|
|
* Discriminant to allow the construction of discriminated / tagged union types
|
|
* @type {string}
|
|
*/
|
|
get kind(): string {
|
|
// Expected to be implemented in concrete class
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Base entity display name
|
|
* @static
|
|
*/
|
|
static displayName: string;
|
|
|
|
/**
|
|
* Human friendly string alias for the entity type e.g. Dashboard, Users
|
|
* @type {string}
|
|
*/
|
|
get displayName(): string {
|
|
// Implemented in concrete class
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Workaround to get the current static instance
|
|
* This makes sense if you want to get a static property only
|
|
* implemented in a subclass, therefore, the same static
|
|
* class is needed
|
|
*/
|
|
get staticInstance(): IBaseEntityStatics<T> {
|
|
return (this.constructor as unknown) as IBaseEntityStatics<T>;
|
|
}
|
|
|
|
/**
|
|
* Will read the current path for an entity
|
|
*/
|
|
get readPath(): Promise<Array<string>> {
|
|
const { urn, staticInstance } = this;
|
|
const entityName = staticInstance.renderProps.search.apiName;
|
|
return readBrowsePath({
|
|
type: entityName,
|
|
urn
|
|
}).then(
|
|
(paths): Array<string> => {
|
|
return paths && paths.length > 0 ? paths[0].split('/').filter(Boolean) : [];
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Asynchronously resolves with an instance of T
|
|
* @readonly
|
|
* @type {Promise<T>}
|
|
*/
|
|
get readEntity(): Promise<T> | Promise<undefined> {
|
|
const { entityPage } = this.staticInstance.renderProps;
|
|
if (entityPage && entityPage.apiName) {
|
|
return readEntity<T>(this.urn, entityPage.apiName);
|
|
}
|
|
// Implemented in concrete class, if it exists for the entity
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Asynchronously resolves with the Snapshot for the entity
|
|
* This should be implemented on the concrete class and is enforced to be available with the
|
|
* abstract modifier
|
|
* @readonly
|
|
* @type {Promise<Snapshot>}
|
|
*/
|
|
get readSnapshot(): Promise<Snapshot> | Promise<undefined> {
|
|
// Implemented in concrete class, if it exists for the entity
|
|
return Promise.resolve(undefined);
|
|
}
|
|
|
|
/**
|
|
* Every entity should have a way to return the name.
|
|
* This can be used for search results or entity header
|
|
*/
|
|
get name(): string {
|
|
// Implemented in concrete class
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Returns a link for the entity page for this entity
|
|
*/
|
|
get entityLink(): IEntityLinkAttrs | void {
|
|
return this.staticInstance.getLinkForEntity({
|
|
entityUrn: this.urn,
|
|
displayName: this.name
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Class properties common across instances
|
|
* Dictates how visual ui components should be rendered
|
|
* Implemented as a getter to ensure that reads are idempotent
|
|
*/
|
|
static get renderProps(): IEntityRenderProps {
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Queries the entity's endpoint to retrieve the list of nodes that are contained in the hierarchy
|
|
* @param {(Array<string>)} args list of string values corresponding to the different hierarchical categories for the entity
|
|
*/
|
|
static async readCategories(...segments: Array<string>): Promise<IBrowsePath> {
|
|
const cleanSegments: Array<string> = segments.filter(Boolean) as Array<string>;
|
|
const defaultFacets = this.renderProps.browse.attributes
|
|
? getFacetDefaultValueForEntity(this.renderProps.browse.attributes)
|
|
: [];
|
|
const { elements, metadata } = await readBrowse({
|
|
type: this.renderProps.search.apiName,
|
|
path: cleanSegments.length > 0 ? `/${cleanSegments.join('/')}` : '',
|
|
count: 100,
|
|
start: 0,
|
|
...defaultFacets
|
|
});
|
|
const entityLinks: Array<IEntityLinkAttrs> = elements.map(
|
|
(element): IEntityLinkAttrs =>
|
|
this.getLinkForEntity({
|
|
displayName: element.name,
|
|
entityUrn: element.urn
|
|
})
|
|
);
|
|
const categoryLinks: Array<IEntityLinkAttrsWithCount> = metadata.groups.map(
|
|
(group): IEntityLinkAttrsWithCount => {
|
|
return this.getLinkForCategory({
|
|
segments: [...cleanSegments, group.name],
|
|
count: group.count,
|
|
displayName: group.name
|
|
});
|
|
}
|
|
);
|
|
return {
|
|
segments,
|
|
title: segments[segments.length - 1] || this.displayName,
|
|
count: metadata.totalNumEntities,
|
|
entities: entityLinks,
|
|
groups: categoryLinks
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Will generate a link for an entity based on a displayName and a entityUrn
|
|
* displayName attribute is used in the anchor tag as a the text representation if provided, is unrelated to BaseEntity['displayName']
|
|
* optionally, a title attribute can be provided to generate the consuming anchor element title
|
|
* @static
|
|
* @param {IGetLinkForEntityParams} params parameters for generating the link object matching the IEntityLinkAttrs interface
|
|
*/
|
|
static getLinkForEntity(params: { entityUrn: string; displayName: string }): IEntityLinkAttrs {
|
|
const { displayName, entityUrn } = params;
|
|
const link: EntityLinkNode = {
|
|
title: displayName || '',
|
|
text: displayName || '',
|
|
route: this.renderProps.browse.entityRoute,
|
|
model: [entityUrn || '']
|
|
};
|
|
|
|
return {
|
|
link,
|
|
entity: this.displayName
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate link for category given segments, displayName and count as optional
|
|
*/
|
|
static getLinkForCategory(params: {
|
|
segments: Array<string>;
|
|
displayName: string;
|
|
count: number;
|
|
}): IEntityLinkAttrsWithCount {
|
|
const { segments, count, displayName } = params;
|
|
const link: EntityLinkNode<{ path: string }> = {
|
|
title: displayName || '',
|
|
text: displayName || segments[0] || '',
|
|
route: 'browse.entity',
|
|
model: [this.displayName],
|
|
queryParams: { path: segments.filter(Boolean).join('/') }
|
|
};
|
|
return {
|
|
link,
|
|
entity: this.displayName,
|
|
count
|
|
};
|
|
}
|
|
|
|
// TODO META-8863 this can be removed once dataset is migrated
|
|
static readCategoriesCount(..._args: Array<string>): Promise<number> {
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Reads the snapshots for the entity
|
|
* @static
|
|
*/
|
|
static readSnapshots(_urns: Array<string>): Promise<Array<Snapshot>> {
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Builds a search query keyword from a list of segments
|
|
* @static
|
|
* @param {Array<string>} [segments=[]] the list of hierarchy segments to generate the keyword for
|
|
*/
|
|
static getQueryForHierarchySegments(segments: Array<string> = []): string {
|
|
return `browsePaths:\\\\/${segments.join('\\\\/').replace(/\s/gi, '\\\\ ')}`;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a list of wiki documents related to the particular entity instance
|
|
* @readonly
|
|
*/
|
|
readInstitutionalMemory(): Promise<Array<InstitutionalMemory>> {
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Writes a list of wiki documents related to a particular entity instance to the api layer
|
|
*/
|
|
writeInstitutionalMemory(): Promise<void> {
|
|
throw new Error(NotImplementedError);
|
|
}
|
|
|
|
/**
|
|
* Creates an instance of BaseEntity concrete class
|
|
* @param {string} urn the urn for the entity being instantiated. urn is a parameter property
|
|
* @memberof BaseEntity
|
|
*/
|
|
constructor(readonly urn: string = '') {}
|
|
}
|