@finos/legend-application-marketplace
Version:
Legend Marketplace application core
705 lines (666 loc) • 24.3 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,
type PlainObject,
ActionState,
assertErrorThrown,
guaranteeNonNullable,
isNonNullable,
} from '@finos/legend-shared';
import { deserialize } from 'serializr';
import {
type V1_EnrichedUserApprovalStatus,
type V1_LiteDataContract,
type V1_LiteDataContractWithUserStatus,
type V1_PendingTasksResponse,
type V1_TaskStatus,
type V1_ContractUserEventRecord,
type V1_TaskStatusChangeResponse,
type V1_DataRequestWithWorkflow,
type V1_DataRequestsWithWorkflowResponse,
V1_deserializeDataContractResponse,
V1_entitlementsDataProductDetailsResponseToDataProductDetails,
V1_liteDataContractWithUserStatusModelSchema,
V1_pendingTasksResponseModelSchema,
V1_TaskStatusChangeResponseModelSchema,
V1_transformDataContractToLiteDatacontract,
V1_deserializeDataRequestsWithWorkflowResponse,
} from '@finos/legend-graph';
import {
type ContractErrorLayer,
type LakehouseContractSyncStatusResponse,
buildSyncErrorLayer,
buildContractErrorsRoot,
getContractAPGCoordinates,
getUnverifiedIngestDefinitionsForAPG,
} from '@finos/legend-extension-dsl-data-product';
import {
makeObservable,
flow,
observable,
flowResult,
action,
computed,
} from 'mobx';
import {
type LakehouseEntitlementsStore,
TEST_USER,
} from './LakehouseEntitlementsStore.js';
export class ContractCreatedByUserDetails {
readonly contractResultLite: V1_LiteDataContract;
assignees: Set<string> = new Set();
members: Map<string, V1_EnrichedUserApprovalStatus> = new Map();
constructor(contract: V1_LiteDataContract) {
this.contractResultLite = contract;
makeObservable(this, {
assignees: observable,
members: observable,
sortedAssigneeIds: computed,
sortedMemberIds: computed,
addAssignees: action,
addMember: action,
});
}
get sortedAssigneeIds(): string[] {
return Array.from(this.assignees).toSorted();
}
get sortedMemberIds(): string[] {
return Array.from(this.members.keys()).toSorted();
}
addAssignees(assignees: string[]): void {
assignees.forEach((assignee) => this.assignees.add(assignee));
}
addMember(id: string, status: V1_EnrichedUserApprovalStatus): void {
this.members.set(id, status);
}
}
export class EntitlementsDashboardState {
readonly lakehouseEntitlementsStore: LakehouseEntitlementsStore;
pendingTasks: V1_ContractUserEventRecord[] | undefined;
pendingTaskContractMap: Map<string, V1_LiteDataContract> = new Map();
allContractsForUser: V1_LiteDataContractWithUserStatus[] | undefined;
// The contracts createdBy user API returns an entry for each task, not just for each contract.
// To consolidate this information, we store a map of contract ID to the contract details + the
// consolidated user information from the tasks.
allContractsCreatedByUserMap: Map<string, ContractCreatedByUserDetails> =
new Map();
dataRequestsCreatedByUser: V1_DataRequestWithWorkflow[] | undefined;
selectedTaskIds: Set<string> = new Set();
readonly initializationState = ActionState.create();
readonly fetchingPendingTasksState = ActionState.create();
readonly fetchingContractsForUserState = ActionState.create();
readonly fetchingContractsByUserState = ActionState.create();
readonly fetchingDataRequestsCreatedByUserState = ActionState.create();
readonly changingState = ActionState.create();
constructor(state: LakehouseEntitlementsStore) {
this.lakehouseEntitlementsStore = state;
makeObservable(this, {
pendingTasks: observable,
allContractsForUser: observable,
allContractsCreatedByUserMap: observable,
pendingTaskContractMap: observable,
selectedTaskIds: observable,
dataRequestsCreatedByUser: observable,
pendingTaskContracts: computed,
allContractsCreatedByUser: computed,
setSelectedTaskIds: action,
init: flow,
approve: flow,
deny: flow,
fetchPendingTasks: flow,
fetchPendingTaskContracts: flow,
fetchContractsForUser: flow,
fetchContractsCreatedByUser: flow,
fetchContractDeploymentEnvironments: flow,
updateContract: flow,
fetchDataRequestsCreatedByUser: flow,
});
}
get pendingTaskContracts(): V1_LiteDataContract[] {
return Array.from(this.pendingTaskContractMap.values());
}
get allContractsCreatedByUser(): ContractCreatedByUserDetails[] {
return Array.from(this.allContractsCreatedByUserMap.values());
}
setSelectedTaskIds(ids: Set<string>): void {
this.selectedTaskIds = ids;
}
*init(token: string | undefined): GeneratorFn<void> {
this.initializationState.inProgress();
this.setSelectedTaskIds(new Set());
try {
this.fetchingPendingTasksState.inProgress();
this.fetchingContractsForUserState.inProgress();
this.fetchingContractsByUserState.inProgress();
this.fetchingDataRequestsCreatedByUserState.inProgress();
const [
pendingTasksData,
contractsForUser,
contractsCreatedByUserMap,
dataRequestsCreatedByUser,
] = (yield Promise.all([
(async () => {
try {
const tasks = await flowResult(this.fetchPendingTasks(token));
const taskContractMap = await flowResult(
this.fetchPendingTaskContracts(token, tasks),
);
return { tasks, taskContractMap };
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.alertUnhandledError(
error,
);
return {
tasks: [] as V1_ContractUserEventRecord[],
taskContractMap: new Map<string, V1_LiteDataContract>(),
};
}
})(),
flowResult(this.fetchContractsForUser(token)),
flowResult(this.fetchContractsCreatedByUser(token)),
flowResult(this.fetchDataRequestsCreatedByUser(token)),
])) as [
{
tasks: V1_ContractUserEventRecord[];
taskContractMap: Map<string, V1_LiteDataContract>;
},
V1_LiteDataContractWithUserStatus[],
Map<string, ContractCreatedByUserDetails>,
V1_DataRequestWithWorkflow[],
];
const allContracts: V1_LiteDataContract[] = [
...Array.from(pendingTasksData.taskContractMap.values()),
...contractsForUser.map((c) => c.contractResultLite),
...Array.from(contractsCreatedByUserMap.values()).map(
(c) => c.contractResultLite,
),
];
const envMap = (yield flowResult(
this.fetchContractDeploymentEnvironments(allContracts, token),
)) as Map<number, string>;
const {
filteredTasks,
filteredContractsForUser,
filteredCreatedByUserMap,
filteredDataRequests,
} = this.filterByUserEnvironment(
pendingTasksData,
contractsForUser,
contractsCreatedByUserMap,
dataRequestsCreatedByUser,
envMap,
);
this.pendingTaskContractMap = pendingTasksData.taskContractMap;
this.pendingTasks = filteredTasks;
this.allContractsForUser = filteredContractsForUser;
this.allContractsCreatedByUserMap = filteredCreatedByUserMap;
this.dataRequestsCreatedByUser = filteredDataRequests;
this.fetchingPendingTasksState.complete();
this.fetchingContractsForUserState.complete();
this.fetchingContractsByUserState.complete();
this.fetchingDataRequestsCreatedByUserState.complete();
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.alertUnhandledError(
error,
);
} finally {
this.initializationState.complete();
}
}
*fetchPendingTasks(
token: string | undefined,
): GeneratorFn<V1_ContractUserEventRecord[]> {
try {
const rawTasks =
(yield this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.fetch(
TEST_USER,
token,
)) as PlainObject<V1_PendingTasksResponse>;
const tasks = deserialize(V1_pendingTasksResponseModelSchema, rawTasks);
return [...tasks.dataOwner, ...tasks.privilegeManager];
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError(
`Error fetching pending tasks: ${error.message}`,
);
return [];
}
}
*fetchPendingTaskContracts(
token: string | undefined,
pendingTasks: V1_ContractUserEventRecord[],
): GeneratorFn<Map<string, V1_LiteDataContract>> {
const pendingTaskContractIds = Array.from(
new Set(pendingTasks.map((t) => t.dataContractId)),
);
const contractClient =
this.lakehouseEntitlementsStore.lakehouseContractServerClient;
const plugins =
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins();
const pendingTaskContracts = (yield Promise.all(
pendingTaskContractIds.map(async (contractId) => {
try {
const rawContractResponse = await contractClient.getDataContract(
contractId,
false,
token,
);
const dataContract = V1_deserializeDataContractResponse(
rawContractResponse,
plugins,
)[0]?.dataContract;
if (!dataContract) {
return undefined;
}
return V1_transformDataContractToLiteDatacontract(dataContract);
} catch (error) {
assertErrorThrown(error);
return undefined;
}
}),
)) as (V1_LiteDataContract | undefined)[];
const resultMap = new Map<string, V1_LiteDataContract>();
pendingTaskContractIds.forEach((contractId, idx) => {
const contract = pendingTaskContracts[idx];
if (contract) {
resultMap.set(contractId, contract);
}
});
return resultMap;
}
*fetchContractsForUser(
token: string | undefined,
): GeneratorFn<V1_LiteDataContractWithUserStatus[]> {
try {
const rawContracts =
(yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsForUser(
this.lakehouseEntitlementsStore.applicationStore.identityService
.currentUser,
token,
)) as PlainObject<V1_LiteDataContractWithUserStatus>[];
return rawContracts.map((rawContract) =>
deserialize(
V1_liteDataContractWithUserStatusModelSchema(
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
),
rawContract,
),
);
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError(
`Error fetching data contracts for user: ${error.message}`,
);
return [];
}
}
*fetchContractsCreatedByUser(
token: string | undefined,
): GeneratorFn<Map<string, ContractCreatedByUserDetails>> {
try {
const rawContracts =
(yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsCreatedByUser(
this.lakehouseEntitlementsStore.applicationStore.identityService
.currentUser,
token,
)) as PlainObject<V1_LiteDataContractWithUserStatus>[];
const contracts = rawContracts.map((rawContract) =>
deserialize(
V1_liteDataContractWithUserStatusModelSchema(
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
),
rawContract,
),
);
const resultMap = new Map<string, ContractCreatedByUserDetails>();
contracts.forEach((contract) => {
if (!resultMap.has(contract.contractResultLite.guid)) {
resultMap.set(
contract.contractResultLite.guid,
new ContractCreatedByUserDetails(contract.contractResultLite),
);
}
const entry = guaranteeNonNullable(
resultMap.get(contract.contractResultLite.guid),
);
entry.addAssignees(contract.pendingTaskWithAssignees?.assignees ?? []);
entry.addMember(contract.user, contract.status);
});
return resultMap;
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError(
`Error fetching data contracts created by user: ${error.message}`,
);
return new Map();
}
}
*fetchDataRequestsCreatedByUser(
token: string | undefined,
): GeneratorFn<V1_DataRequestWithWorkflow[]> {
try {
const raw =
(yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getDataAccessRequestsCreatedBy(
this.lakehouseEntitlementsStore.applicationStore.identityService
.currentUser,
token,
)) as PlainObject<V1_DataRequestsWithWorkflowResponse>;
return V1_deserializeDataRequestsWithWorkflowResponse(
raw,
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
);
} catch (error) {
assertErrorThrown(error);
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError(
`Error fetching data requests created by user: ${error.message}`,
);
return [];
}
}
*fetchContractDeploymentEnvironments(
allContracts: V1_LiteDataContract[],
token: string | undefined,
): GeneratorFn<Map<number, string>> {
const uniqueDIDToDataProduct = new Map<number, string>();
for (const contract of allContracts) {
uniqueDIDToDataProduct.set(contract.deploymentId, contract.resourceId);
}
const didToEnvType = new Map<number, string>();
const contractClient =
this.lakehouseEntitlementsStore.lakehouseContractServerClient;
yield Promise.all(
Array.from(uniqueDIDToDataProduct.entries()).map(
async ([deploymentId, resourceId]) => {
try {
const raw = await contractClient.getDataProductByIdAndDID(
resourceId,
deploymentId,
token,
);
const env =
V1_entitlementsDataProductDetailsResponseToDataProductDetails(
raw,
)[0]?.lakehouseEnvironment?.type;
if (env) {
didToEnvType.set(deploymentId, env);
}
} catch (error) {
assertErrorThrown(error);
}
},
),
);
return didToEnvType;
}
async getContractIngestErrors(
contractId: string,
token: string | undefined,
): Promise<ContractErrorLayer | undefined> {
const baseStore = this.lakehouseEntitlementsStore.marketplaceBaseStore;
const plugins =
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins();
const apg = await getContractAPGCoordinates(
contractId,
baseStore.lakehouseContractServerClient,
plugins,
token,
);
if (!apg) {
return undefined;
}
const unverifiedIngestDefinitions =
await getUnverifiedIngestDefinitionsForAPG(
apg,
baseStore,
plugins,
() => baseStore.createInitializedGraphManager(),
token,
);
if (unverifiedIngestDefinitions.length === 0) {
return undefined;
}
return {
title: `Ingest${unverifiedIngestDefinitions.length === 1 ? '' : 's'} Not Found:`,
errorItems: unverifiedIngestDefinitions,
};
}
async getContractSyncErrors(
contractId: string,
token: string | undefined,
): Promise<ContractErrorLayer | undefined> {
try {
const response =
(await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractSyncStatus(
contractId,
token,
)) as LakehouseContractSyncStatusResponse;
return buildSyncErrorLayer(response);
} catch (error) {
assertErrorThrown(error);
return undefined;
}
}
async getContractErrors(
contractId: string,
token: string | undefined,
checkSyncStatus = false,
): Promise<ContractErrorLayer | undefined> {
const [ingestErrorsLayer, syncErrorsLayer] = await Promise.all([
this.getContractIngestErrors(contractId, token),
checkSyncStatus
? this.getContractSyncErrors(contractId, token)
: Promise.resolve(undefined),
]);
return buildContractErrorsRoot([ingestErrorsLayer, syncErrorsLayer]);
}
filterByUserEnvironment(
pendingData: {
tasks: V1_ContractUserEventRecord[];
taskContractMap: Map<string, V1_LiteDataContract>;
},
contractsForUser: V1_LiteDataContractWithUserStatus[],
contractsCreatedByUserMap: Map<string, ContractCreatedByUserDetails>,
dataRequests: V1_DataRequestWithWorkflow[],
envMap: Map<number, string>,
): {
filteredTasks: V1_ContractUserEventRecord[];
filteredContractsForUser: V1_LiteDataContractWithUserStatus[];
filteredCreatedByUserMap: Map<string, ContractCreatedByUserDetails>;
filteredDataRequests: V1_DataRequestWithWorkflow[];
} {
const userEnv =
this.lakehouseEntitlementsStore.marketplaceBaseStore.envState
.lakehouseEnvironment;
const envMatchesForDeploymentId = (deploymentId: number): boolean => {
const env = envMap.get(deploymentId);
return !env || env === userEnv;
};
const filteredTasks = pendingData.tasks.filter((task) => {
const contract = pendingData.taskContractMap.get(task.dataContractId);
return !contract || envMatchesForDeploymentId(contract.deploymentId);
});
const filteredContractsForUser = contractsForUser.filter((c) =>
envMatchesForDeploymentId(c.contractResultLite.deploymentId),
);
const filteredCreatedByUserMap = new Map<
string,
ContractCreatedByUserDetails
>();
for (const [guid, details] of contractsCreatedByUserMap.entries()) {
if (envMatchesForDeploymentId(details.contractResultLite.deploymentId)) {
filteredCreatedByUserMap.set(guid, details);
}
}
const filteredDataRequests = dataRequests.filter((dr) => {
const envType = dr.dataRequest.resourceEnvType;
return !envType || envType === userEnv;
});
return {
filteredTasks,
filteredContractsForUser,
filteredCreatedByUserMap,
filteredDataRequests,
};
}
*updateContract(
contractId: string,
token: string | undefined,
): GeneratorFn<void> {
const [newUserContracts, newCreatedByUserContracts] = (yield Promise.all([
(async () => {
const rawContracts =
await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsForUser(
this.lakehouseEntitlementsStore.applicationStore.identityService
.currentUser,
token,
);
return rawContracts.map((rawContract) =>
deserialize(
V1_liteDataContractWithUserStatusModelSchema(
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
),
rawContract,
),
);
})(),
(async () => {
const rawContracts =
await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsCreatedByUser(
this.lakehouseEntitlementsStore.applicationStore.identityService
.currentUser,
token,
);
return rawContracts.map((rawContract) =>
deserialize(
V1_liteDataContractWithUserStatusModelSchema(
this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
),
rawContract,
),
);
})(),
])) as [
V1_LiteDataContractWithUserStatus[],
V1_LiteDataContractWithUserStatus[],
];
// Update the contract for the user
this.allContractsForUser = this.allContractsForUser
?.map((contract) =>
contract.contractResultLite.guid === contractId
? newUserContracts.find(
(c) => c.contractResultLite.guid === contractId,
)
: contract,
)
.filter(isNonNullable);
// Update the contract + all related data for contract created by the user
this.allContractsCreatedByUserMap.delete(contractId);
const updatedCreatedByUserContracts = newCreatedByUserContracts.filter(
(c) => c.contractResultLite.guid === contractId,
);
updatedCreatedByUserContracts.forEach((contract) => {
if (
!this.allContractsCreatedByUserMap.has(contract.contractResultLite.guid)
) {
this.allContractsCreatedByUserMap.set(
contract.contractResultLite.guid,
new ContractCreatedByUserDetails(contract.contractResultLite),
);
}
const entry = guaranteeNonNullable(
this.allContractsCreatedByUserMap.get(contract.contractResultLite.guid),
);
entry.addAssignees(contract.pendingTaskWithAssignees?.assignees ?? []);
entry.addMember(contract.user, contract.status);
});
}
*approve(
task: V1_ContractUserEventRecord,
token: string | undefined,
): GeneratorFn<void> {
try {
this.changingState.inProgress();
this.changingState.setMessage('Approving Task');
const response =
(yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.approveTask(
task.taskId,
token,
)) as PlainObject<V1_TaskStatusChangeResponse>;
const change = deserialize(
V1_TaskStatusChangeResponseModelSchema,
response,
);
if (change.errorMessage) {
throw new Error(
`Unable to approve task: ${task.taskId}: ${change.errorMessage}`,
);
}
task.status = change.status;
this.pendingTasks = [...(this.pendingTasks ?? [])];
this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.invalidate();
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess(
`Task has been Approved`,
);
} finally {
this.changingState.complete();
this.changingState.setMessage(undefined);
}
}
*deny(
task: V1_ContractUserEventRecord,
token: string | undefined,
): GeneratorFn<void> {
try {
this.changingState.inProgress();
this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert(
{
message: 'Denying Task',
prompt: 'Denying task...',
showLoading: true,
},
);
const response =
(yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.denyTask(
task.taskId,
token,
)) as PlainObject<V1_TaskStatus>;
const change = deserialize(
V1_TaskStatusChangeResponseModelSchema,
response,
);
if (change.errorMessage) {
throw new Error(
`Unable to deny task: ${task.taskId}: ${change.errorMessage}`,
);
}
task.status = change.status;
this.pendingTasks = [...(this.pendingTasks ?? [])];
this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.invalidate();
this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess(
`Task has been denied`,
);
} finally {
this.changingState.complete();
this.changingState.setMessage(undefined);
this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert(
undefined,
);
}
}
}