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) => void} constrains T to constructor interfaces * @type {() => ClassDecorator} */ export const statics = ) => void>(): ((c: T) => void) => (_ctor: T): void => {}; /** * Defines the interface for the static side or constructor of a class that extends BaseEntity * @export * @interface IBaseEntityStatics * @template T constrained by the IBaseEntity interface, the entity interface that BaseEntity subclass will encapsulate */ export interface IBaseEntityStatics { new (urn: string): BaseEntity; /** * 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)} args list of string values corresponding to the different hierarchical categories for the entity */ readCategories(...args: Array): Promise; /** * 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): Promise; /** * Queries the batch GET endpoint for snapshots for the supplied urns */ readSnapshots(_urns: Array): Promise>; /** * Builds a search query keyword from a list of segments for the related DataModelEntity * @memberof IBaseEntityStatics */ getQueryForHierarchySegments(_segments: Array): 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 = (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 { /** * 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; /** * Hook for custom fetching operations after entity is created */ onAfterCreate(): Promise { 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 { 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(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} * @memberof BaseEntity */ @computed('snapshot') get owners(): Array { 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} * @memberof BaseEntity */ @map('owners.[]', ({ owner }: IOwner): string => owner) ownerUrns!: Array; /** * 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 { return (this.constructor as unknown) as IBaseEntityStatics; } /** * Will read the current path for an entity */ get readPath(): Promise> { const { urn, staticInstance } = this; const entityName = staticInstance.renderProps.search.apiName; return readBrowsePath({ type: entityName, urn }).then( (paths): Array => { return paths && paths.length > 0 ? paths[0].split('/').filter(Boolean) : []; } ); } /** * Asynchronously resolves with an instance of T * @readonly * @type {Promise} */ get readEntity(): Promise | Promise { const { entityPage } = this.staticInstance.renderProps; if (entityPage && entityPage.apiName) { return readEntity(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} */ get readSnapshot(): Promise | Promise { // 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)} args list of string values corresponding to the different hierarchical categories for the entity */ static async readCategories(...segments: Array): Promise { const cleanSegments: Array = segments.filter(Boolean) as Array; 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 = elements.map( (element): IEntityLinkAttrs => this.getLinkForEntity({ displayName: element.name, entityUrn: element.urn }) ); const categoryLinks: Array = 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; 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): Promise { throw new Error(NotImplementedError); } /** * Reads the snapshots for the entity * @static */ static readSnapshots(_urns: Array): Promise> { throw new Error(NotImplementedError); } /** * Builds a search query keyword from a list of segments * @static * @param {Array} [segments=[]] the list of hierarchy segments to generate the keyword for */ static getQueryForHierarchySegments(segments: Array = []): string { return `browsePaths:\\\\/${segments.join('\\\\/').replace(/\s/gi, '\\\\ ')}`; } /** * Retrieves a list of wiki documents related to the particular entity instance * @readonly */ readInstitutionalMemory(): Promise> { throw new Error(NotImplementedError); } /** * Writes a list of wiki documents related to a particular entity instance to the api layer */ writeInstitutionalMemory(): Promise { 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 = '') {} }