@finos/legend-application-marketplace
Version:
Legend Marketplace application core
512 lines (464 loc) • 17 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type GeneratorFn,
ActionState,
assertErrorThrown,
isNonNullable,
LogEvent,
UserSearchService,
} from '@finos/legend-shared';
import {
type ApplicationStore,
LegendApplicationTelemetryHelper,
APPLICATION_EVENT,
DEFAULT_TAB_SIZE,
} from '@finos/legend-application';
import { action, flow, makeObservable, observable } from 'mobx';
import {
DepotServerClient,
StoreProjectData,
} from '@finos/legend-server-depot';
import {
MarketplaceServerClient,
RegistryServerClient,
TerminalAccessServerClient,
} from '@finos/legend-server-marketplace';
import {
type V1_EngineServerClient,
getCurrentUserIDFromEngineServer,
V1_entitlementsDataProductDetailsResponseToDataProductDetails,
V1_PureGraphManager,
V1_RemoteEngine,
} from '@finos/legend-graph';
import type { LegendMarketplaceApplicationConfig } from '../application/LegendMarketplaceApplicationConfig.js';
import type { LegendMarketplacePluginManager } from '../application/LegendMarketplacePluginManager.js';
import { LegendMarketplaceEventHelper } from '../__lib__/LegendMarketplaceEventHelper.js';
import {
LakehouseContractServerClient,
LakehouseIngestServerClient,
LakehousePlatformServerClient,
LakehouseWorkflowServerClient,
PermitWorkflowServerClient,
} from '@finos/legend-server-lakehouse';
import { CartStore } from './cart/CartStore.js';
import { PendingTasksCache } from './lakehouse/PendingTasksCache.js';
import { parseGAVCoordinates, type Entity } from '@finos/legend-storage';
import { V1_deserializeDataSpace } from '@finos/legend-extension-dsl-data-space/graph';
import {
DevelopmentLegendMarketplaceEnvState,
LegendMarketplaceEnv,
ProdLegendMarketplaceEnvState,
ProdParallelLegendMarketplaceEnvState,
type LegendMarketplaceEnvState,
} from './LegendMarketplaceEnvState.js';
import { ProductCardState } from './lakehouse/dataProducts/ProductCardState.js';
import {
convertEntitlementsDataProductDetailsToSearchResult,
convertLegacyDataProductToSearchResult,
convertTrendingEntryToSearchResult,
} from '../utils/SearchUtils.js';
import { LakehouseDataProductService } from './lakehouse/LakehouseDataProductService.js';
export type LegendMarketplaceApplicationStore = ApplicationStore<
LegendMarketplaceApplicationConfig,
LegendMarketplacePluginManager
>;
export class LegendMarketplaceBaseStore {
readonly applicationStore: LegendMarketplaceApplicationStore;
readonly envState: LegendMarketplaceEnvState;
readonly adjacentEnvState: LegendMarketplaceEnvState | undefined;
readonly marketplaceServerClient: MarketplaceServerClient;
readonly depotServerClient: DepotServerClient;
readonly lakehouseContractServerClient: LakehouseContractServerClient;
readonly lakehousePlatformServerClient: LakehousePlatformServerClient;
readonly lakehouseIngestServerClient: LakehouseIngestServerClient;
readonly engineServerClient: V1_EngineServerClient;
readonly registryServerClient: RegistryServerClient | undefined;
readonly pluginManager: LegendMarketplacePluginManager;
readonly remoteEngine: V1_RemoteEngine;
readonly userSearchService: UserSearchService | undefined;
readonly lakehouseWorkflowServerClient: LakehouseWorkflowServerClient;
readonly permitWorkflowServerClient: PermitWorkflowServerClient | undefined;
readonly lakehouseDataProductService: LakehouseDataProductService;
readonly cartStore: CartStore;
readonly terminalAccessServerClient: TerminalAccessServerClient;
readonly pendingTasksCache: PendingTasksCache;
readonly initState = ActionState.create();
showDemoModal = false;
constructor(applicationStore: LegendMarketplaceApplicationStore) {
makeObservable<LegendMarketplaceBaseStore>(this, {
showDemoModal: observable,
setDemoModal: action,
initialize: flow,
});
this.applicationStore = applicationStore;
this.pluginManager = applicationStore.pluginManager;
// marketplace
this.envState =
applicationStore.config.dataProductEnv === LegendMarketplaceEnv.PRODUCTION
? new ProdLegendMarketplaceEnvState()
: applicationStore.config.dataProductEnv ===
LegendMarketplaceEnv.PRODUCTION_PARALLEL
? new ProdParallelLegendMarketplaceEnvState()
: new DevelopmentLegendMarketplaceEnvState();
this.adjacentEnvState = this.buildAdjacentEnvState();
this.marketplaceServerClient = new MarketplaceServerClient({
serverUrl: this.applicationStore.config.marketplaceServerUrl,
subscriptionUrl: this.applicationStore.config.marketplaceSubscriptionUrl,
});
this.marketplaceServerClient.setTracerService(
this.applicationStore.tracerService,
);
// registry
if (this.applicationStore.config.registryUrl) {
this.registryServerClient = new RegistryServerClient({
baseUrl: this.applicationStore.config.registryUrl,
});
this.registryServerClient.setTracerService(
this.applicationStore.tracerService,
);
}
// depot
this.depotServerClient = new DepotServerClient({
serverUrl: this.applicationStore.config.depotServerUrl,
});
this.depotServerClient.setTracerService(
this.applicationStore.tracerService,
);
// lakehouse contract
this.lakehouseContractServerClient = new LakehouseContractServerClient({
baseUrl: this.applicationStore.config.lakehouseServerUrl,
});
this.lakehouseContractServerClient.setTracerService(
this.applicationStore.tracerService,
);
// terminal
this.terminalAccessServerClient = new TerminalAccessServerClient({
baseUrl: this.applicationStore.config.terminalServerUrl,
});
this.terminalAccessServerClient.setTracerService(
this.applicationStore.tracerService,
);
// lakehouse platform
this.lakehousePlatformServerClient = new LakehousePlatformServerClient(
this.applicationStore.config.lakehousePlatformUrl,
);
this.lakehousePlatformServerClient.setTracerService(
this.applicationStore.tracerService,
);
// lakehouse workflow
this.lakehouseWorkflowServerClient = new LakehouseWorkflowServerClient({
baseUrl: this.applicationStore.config.lakehouseWorkflowServerUrl,
});
this.lakehouseWorkflowServerClient.setTracerService(
this.applicationStore.tracerService,
);
// permit + eTask workflow
if (this.applicationStore.config.lakehousePermitWorkflowServerUrl) {
this.permitWorkflowServerClient = new PermitWorkflowServerClient({
authBaseUrl: this.applicationStore.config.lakehouseServerUrl,
workflowBaseUrl:
this.applicationStore.config.lakehousePermitWorkflowServerUrl,
});
this.permitWorkflowServerClient.setTracerService(
this.applicationStore.tracerService,
);
} else {
this.permitWorkflowServerClient = undefined;
}
// lakehouse ingest
this.lakehouseIngestServerClient = new LakehouseIngestServerClient(
undefined,
);
this.lakehouseIngestServerClient.setTracerService(
this.applicationStore.tracerService,
);
this.remoteEngine = new V1_RemoteEngine(
{
baseUrl: this.applicationStore.config.engineServerUrl,
},
applicationStore.logService,
);
this.engineServerClient = this.remoteEngine.getEngineServerClient();
this.engineServerClient.setTracerService(applicationStore.tracerService);
// User search
if (this.pluginManager.getUserPlugins().length > 0) {
this.pluginManager
.getUserPlugins()
.forEach((plugin) =>
plugin.setup(this.applicationStore.config.marketplaceUserSearchUrl),
);
this.userSearchService = new UserSearchService({
userProfileImageUrl:
this.applicationStore.config.marketplaceUserProfileImageUrl,
applicationDirectoryUrl:
this.applicationStore.config.lakehouseEntitlementsConfig
?.applicationDirectoryUrl,
});
this.userSearchService.registerPlugins(
this.pluginManager.getUserPlugins(),
);
}
// Data Product service
this.lakehouseDataProductService = new LakehouseDataProductService(
this,
this.lakehousePlatformServerClient,
this.lakehouseContractServerClient,
);
// Initialize cart store
this.cartStore = new CartStore(this);
// Shared cache + in-flight dedupe for /datacontracts/tasks/pending
this.pendingTasksCache = new PendingTasksCache(
this.lakehouseContractServerClient,
);
}
buildAdjacentEnvState(): LegendMarketplaceEnvState | undefined {
const adjacentEnv = this.envState.adjacentEnv;
if (adjacentEnv) {
return adjacentEnv === LegendMarketplaceEnv.PRODUCTION
? new ProdLegendMarketplaceEnvState()
: new ProdParallelLegendMarketplaceEnvState();
}
return undefined;
}
private buildVendorImageMap(): Map<string, string> {
const vendorImageMap = new Map<string, string>();
const assetsBaseUrl = this.applicationStore.config.assetsBaseUrl;
for (const [vendorName, filename] of Object.entries(
this.applicationStore.config.assetsProductImageMap,
)) {
vendorImageMap.set(vendorName, `${assetsBaseUrl}/${filename}`);
}
return vendorImageMap;
}
async createInitializedGraphManager(): Promise<V1_PureGraphManager> {
const graphManager = new V1_PureGraphManager(
this.applicationStore.pluginManager,
this.applicationStore.logService,
this.remoteEngine,
);
await graphManager.initialize(
{
env: this.applicationStore.config.env,
tabSize: DEFAULT_TAB_SIZE,
clientConfig: {
baseUrl: this.applicationStore.config.engineServerUrl,
},
},
{ engine: this.remoteEngine },
);
return graphManager;
}
private parseDataProductEntries(
entriesString: string,
): (
| { dataProductId: string; deploymentId: number; gav?: undefined }
| { dataProductId: string; gav: string; deploymentId?: undefined }
)[] {
return entriesString
.split(',')
.map((entry) => {
const vals = entry.split('/');
if (vals[0] === undefined || vals[1] === undefined) {
return undefined;
}
const id = vals[0];
const secondPart = vals[1];
if (Number.isInteger(Number(secondPart))) {
return {
dataProductId: id,
deploymentId: parseInt(secondPart),
};
} else {
return { dataProductId: id, gav: secondPart };
}
})
.filter(isNonNullable);
}
async initHighlightedDataProducts(
token: string | undefined,
): Promise<Record<string, ProductCardState[]> | undefined> {
const highlightedConfig =
this.applicationStore.config.options.highlightedDataProducts;
if (!highlightedConfig) {
return undefined;
}
const sectionEntries = Object.entries(highlightedConfig);
if (sectionEntries.length === 0) {
return undefined;
}
const vendorImageMap = this.buildVendorImageMap();
const getDataProductState = async (
dataProductId: string,
deploymentId: number,
graphManager: V1_PureGraphManager,
) => {
const rawResponse =
await this.lakehouseContractServerClient.getDataProductByIdAndDID(
dataProductId,
deploymentId,
token,
);
const dataProductDetail =
V1_entitlementsDataProductDetailsResponseToDataProductDetails(
rawResponse,
)[0];
if (dataProductDetail) {
const searchResult =
convertEntitlementsDataProductDetailsToSearchResult(
dataProductDetail,
);
return new ProductCardState(
this,
searchResult,
graphManager,
vendorImageMap,
);
} else {
return undefined;
}
};
const getLegacyDataProductState = async (
dataProductId: string,
gav: string,
graphManager: V1_PureGraphManager,
) => {
const coordinates = parseGAVCoordinates(gav);
const storeProject = new StoreProjectData();
storeProject.groupId = coordinates.groupId;
storeProject.artifactId = coordinates.artifactId;
const legacyDataProuct = await this.depotServerClient.getEntity(
storeProject,
coordinates.versionId,
dataProductId,
);
const dataSpace = V1_deserializeDataSpace(
(legacyDataProuct as unknown as Entity).content,
);
const searchResult = convertLegacyDataProductToSearchResult(
dataSpace,
coordinates.groupId,
coordinates.artifactId,
coordinates.versionId,
);
return new ProductCardState(
this,
searchResult,
graphManager,
vendorImageMap,
);
};
const graphManager = await this.createInitializedGraphManager();
const result: Record<string, ProductCardState[]> = {};
await Promise.all(
sectionEntries.map(async ([sectionName, entriesString]) => {
const entries = this.parseDataProductEntries(entriesString);
const states = (
await Promise.all(
entries.map(async (dataProduct) =>
dataProduct.deploymentId !== undefined
? getDataProductState(
dataProduct.dataProductId,
dataProduct.deploymentId,
graphManager,
)
: getLegacyDataProductState(
dataProduct.dataProductId,
dataProduct.gav,
graphManager,
),
),
)
).filter(isNonNullable);
states.forEach((state) => state.init(token));
result[sectionName] = states;
}),
);
return result;
}
async fetchTrendingDataProducts(
token: string | undefined,
): Promise<Record<string, ProductCardState[]> | undefined> {
const vendorImageMap = this.buildVendorImageMap();
const graphManager = await this.createInitializedGraphManager();
const trendingEntries =
await this.marketplaceServerClient.getTrendingDataProducts(
this.envState.lakehouseEnvironment,
);
const states = trendingEntries.slice(0, 4).map((entry) => {
const searchResult = convertTrendingEntryToSearchResult(entry);
return new ProductCardState(
this,
searchResult,
graphManager,
vendorImageMap,
);
});
states.forEach((state) => state.init(token));
return { Trending: states };
}
setDemoModal(val: boolean): void {
this.showDemoModal = val;
}
*initialize(): GeneratorFn<void> {
if (!this.initState.isInInitialState) {
this.applicationStore.notificationService.notifyIllegalState(
'Base store is re-initialized',
);
return;
}
this.initState.inProgress();
// retrieved the user identity is not already configured
if (this.applicationStore.identityService.isAnonymous) {
try {
this.applicationStore.identityService.setCurrentUser(
(yield getCurrentUserIDFromEngineServer(
this.applicationStore.config.engineServerUrl,
)) as string,
);
} catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.error(
LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE),
error,
);
this.applicationStore.notificationService.notifyWarning(error.message);
}
}
// setup telemetry service
this.applicationStore.telemetryService.setup();
LegendApplicationTelemetryHelper.logEvent_ApplicationInitializationSucceeded(
this.applicationStore.telemetryService,
this.applicationStore,
);
LegendMarketplaceEventHelper.notify_ApplicationLoadSucceeded(
this.applicationStore.eventService,
);
// Initialize cart store to load existing items
try {
yield* this.cartStore.initialize();
} catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.warn(
LogEvent.create(APPLICATION_EVENT.IDENTITY_AUTO_FETCH__FAILURE),
'Failed to initialize cart store',
error,
);
// Don't show notification as cart initialization failure shouldn't block app startup
}
this.initState.complete();
}
}