/** * 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 { ActionState, assertErrorThrown, guaranteeNonNullable, guaranteeType, HttpStatus, isNonNullable, NetworkClientError, type GeneratorFn, type PlainObject, } from '@finos/legend-shared'; import { deserialize } from 'serializr'; import { type PureProtocolProcessorPlugin, type V1_DataProduct, type V1_EnrichedUserApprovalStatus, type V1_LiteDataContractWithUserStatus, type V1_PendingTasksResponse, type V1_TaskStatusChangeResponse, RawLambda, V1_DataProductAccessor, V1_deserializeDataContractResponse, type V1_DataRequestWithWorkflow, V1_entitlementsDataProductDetailsResponseToDataProductDetails, V1_IngestDefinitionAccessor, V1_LakehouseAccessPoint, V1_liteDataContractWithUserStatusModelSchema, V1_pendingTasksResponseModelSchema, V1_PureGraphManager, V1_resolveAccessorsFromRawLambda, V1_ResourceType, V1_SdlcDeploymentDataProductOrigin, V1_TaskStatusChangeResponseModelSchema, V1_transformDataContractToLiteDatacontract, V1_transformDataRequestWithWorkflowToLiteDataAccessRequest, V1_deserializeDataRequestsWithWorkflowResponse, type V1_DataRequestsWithWorkflowResponse, type V1_PendingDataRequestTaskEntry, type V1_PendingDataRequestTasksResponse, V1_deserializePendingDataRequestTasksResponse, type V1_ContractUserEventRecord, V1_DataRequestUserEventRecord, type V1_PendingTaskRecord, V1_ApprovalType, V1_UserApprovalStatus, type V1_LiteDataContract, type V1_LiteAccessRequest, V1_PermitTaskAction, type V1_OrganizationalScope, } from '@finos/legend-graph'; import { DEFAULT_TAB_SIZE } from '@finos/legend-application'; import { stringifyOrganizationalScope, type ContractErrorLayer, type DataProductDataAccess_LegendApplicationPlugin_Extension, } from '@finos/legend-extension-dsl-data-product'; import { makeObservable, flow, observable, flowResult, action, computed, } from 'mobx'; import { TEST_USER, type LakehouseEntitlementsStore, } from './LakehouseEntitlementsStore.js'; import { getDataProductFromDetails } from '../../../utils/LakehouseUtils.js'; export enum ContractSyncStatus { NEVER_SYNCED = 'NEVER_SYNCED', NOT_FULLY_SYNCED = 'NOT_FULLY_SYNCED', } export type LakehouseContractSyncStatusResponse = { status: string; unsyncedUsers?: { username: string }[]; unsyncedAccessPoints?: { accessPointName: string }[]; unsyncedTargetAccounts?: string[]; }; export enum TaskApprovalAction { APPROVE = 'approve', DENY = 'deny', } const collectIngestSpecPathsFromOriginDp = ( rootDataProduct: V1_DataProduct, accessPointGroupId: string, graphManager: V1_PureGraphManager, plugins: PureProtocolProcessorPlugin[], ): Set => { const dpPath = `${rootDataProduct.package}::${rootDataProduct.name}`; const targetApg = rootDataProduct.accessPointGroups.find( (apg) => apg.id === accessPointGroupId, ); if (!targetApg) { throw new Error( `Access point group '${accessPointGroupId}' not found in data product '${dpPath}'`, ); } const specs = new Set(); const visited = new Set(); const worklist: string[] = [accessPointGroupId]; const collectFromApg = (apgId: string): void => { const apg = rootDataProduct.accessPointGroups.find((g) => g.id === apgId); if (!apg) { return; } for (const accessPoint of apg.accessPoints) { if (!(accessPoint instanceof V1_LakehouseAccessPoint)) { continue; } const visitKey = `${dpPath}::${accessPoint.id}`; if (visited.has(visitKey)) { continue; } visited.add(visitKey); const rawLambda = new RawLambda( accessPoint.func.parameters, accessPoint.func.body, ); const accessors = V1_resolveAccessorsFromRawLambda(rawLambda, graphManager, plugins) ?? []; for (const accessor of accessors) { if (accessor instanceof V1_IngestDefinitionAccessor) { const specPath = accessor.path[0]; if (specPath) { specs.add(specPath); } } else if (accessor instanceof V1_DataProductAccessor) { const refDpPath = accessor.path[0]; const refApId = accessor.path[1]; if (!refDpPath || !refApId) { continue; } if (refDpPath !== dpPath) { continue; } const refApg = rootDataProduct.accessPointGroups.find((g) => g.accessPoints.some((ap) => ap.id === refApId), ); if (refApg && !worklist.includes(refApg.id)) { worklist.push(refApg.id); } } } } }; while (worklist.length > 0) { const apgId = guaranteeNonNullable(worklist.shift()); collectFromApg(apgId); } return specs; }; export class ContractCreatedByUserDetails { readonly contractResultLite: V1_LiteAccessRequest; assignees: Set = new Set(); members: Map = new Map(); constructor(contract: V1_LiteAccessRequest) { 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_PendingTaskRecord[] | undefined; pendingTaskContractMap: Map = 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 = new Map(); dataRequestsCreatedByUser: V1_DataRequestWithWorkflow[] | undefined; selectedTaskIds: Set = new Set(); pendingDataRequestIds: Set = new Set(); pendingDataRequestDetailsMap: Map = new Map(); 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, pendingDataRequestIds: observable, pendingDataRequestDetailsMap: 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, fetchPendingDataRequestTasks: flow, }); } get pendingTaskContracts(): V1_LiteAccessRequest[] { return Array.from(this.pendingTaskContractMap.values()); } get allContractsCreatedByUser(): ContractCreatedByUserDetails[] { return Array.from(this.allContractsCreatedByUserMap.values()); } setSelectedTaskIds(ids: Set): void { this.selectedTaskIds = ids; } *init(token: string | undefined): GeneratorFn { 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, pendingDataRequestTasksResponse, ] = (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_PendingTaskRecord[], taskContractMap: new Map(), }; } })(), flowResult(this.fetchContractsForUser(token)), flowResult(this.fetchContractsCreatedByUser(token)), flowResult(this.fetchDataRequestsCreatedByUser(token)), flowResult(this.fetchPendingDataRequestTasks(token)), ])) as [ { tasks: V1_PendingTaskRecord[]; taskContractMap: Map; }, V1_LiteDataContractWithUserStatus[], Map, V1_DataRequestWithWorkflow[], { tasks: V1_PendingTaskRecord[]; taskContractMap: Map; }, ]; // Merge data request pending tasks into contract pending tasks const dataRequestIds = new Set(); for (const task of pendingDataRequestTasksResponse.tasks) { pendingTasksData.tasks.push(task); dataRequestIds.add(task.accessRequestId); } for (const [ id, contract, ] of pendingDataRequestTasksResponse.taskContractMap) { pendingTasksData.taskContractMap.set(id, contract); } this.pendingDataRequestIds = dataRequestIds; const allContracts: V1_LiteAccessRequest[] = [ ...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; 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 { try { const rawTasks = (yield this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.fetch( TEST_USER, token, )) as PlainObject; 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 []; } } *fetchPendingDataRequestTasks(token: string | undefined): GeneratorFn<{ tasks: V1_DataRequestUserEventRecord[]; taskContractMap: Map; }> { try { const plugins = this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(); const currentUser = this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser; const rawResponse = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getPendingDataRequestTasks( currentUser, token, )) as PlainObject; const response = V1_deserializePendingDataRequestTasksResponse( rawResponse, plugins, ); // Collect unique data request IDs and fetch withWorkflow details for each const allEntries = [...response.dataOwner, ...response.privilegeManager]; const uniqueDataRequestIds = Array.from( new Set(allEntries.map((e) => e.dataRequestId)), ); const dataRequestDetailsMap = new Map< string, V1_DataRequestWithWorkflow >(); const contractClient = this.lakehouseEntitlementsStore.lakehouseContractServerClient; yield Promise.all( uniqueDataRequestIds.map(async (dataRequestId) => { try { const rawDetail = await contractClient.getDataAccessRequestWithWorkflow( dataRequestId, token, ); const details = V1_deserializeDataRequestsWithWorkflowResponse( rawDetail, plugins, ); if (details[0]) { dataRequestDetailsMap.set(dataRequestId, details[0]); } } catch (error) { assertErrorThrown(error); } }), ); this.pendingDataRequestDetailsMap = dataRequestDetailsMap; const tasks: V1_DataRequestUserEventRecord[] = []; const taskContractMap = new Map(); const processEntries = ( entries: V1_PendingDataRequestTaskEntry[], approvalType: V1_ApprovalType, ): void => { for (const entry of entries) { const task = entry.task; const detail = dataRequestDetailsMap.get(entry.dataRequestId); // Create V1_DataRequestUserEventRecord for grid row data const record = new V1_DataRequestUserEventRecord(); record.taskId = task.taskId; record.dataRequestId = entry.dataRequestId; record.status = V1_UserApprovalStatus.PENDING; record.consumer = this.getConsumerDisplayString(task.consumer); record.type = approvalType; tasks.push(record); // Create V1_LiteDataAccessRequest for grid column lookups if (detail && !taskContractMap.has(entry.dataRequestId)) { taskContractMap.set( entry.dataRequestId, V1_transformDataRequestWithWorkflowToLiteDataAccessRequest( detail, ), ); } } }; processEntries(response.dataOwner, V1_ApprovalType.DATA_OWNER_APPROVAL); processEntries( response.privilegeManager, V1_ApprovalType.CONSUMER_PRIVILEGE_MANAGER_APPROVAL, ); return { tasks, taskContractMap }; } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError( `Error fetching pending data request tasks: ${error.message}`, ); return { tasks: [], taskContractMap: new Map(), }; } } private getConsumerDisplayString(consumer: V1_OrganizationalScope): string { const dataAccessPlugins = this.lakehouseEntitlementsStore.applicationStore.pluginManager.getApplicationPlugins() as DataProductDataAccess_LegendApplicationPlugin_Extension[]; const orgNodeCode = dataAccessPlugins .map((p) => p.getOrganizationalNodeCode?.(consumer)) .find(isNonNullable); if (orgNodeCode) { return orgNodeCode; } return stringifyOrganizationalScope(consumer, dataAccessPlugins); } private static parseDeploymentId(did: string): number { return Number.parseInt(did, 10) || 0; } *fetchPendingTaskContracts( token: string | undefined, pendingTasks: V1_PendingTaskRecord[], ): GeneratorFn> { const pendingTaskContractIds = Array.from( new Set(pendingTasks.map((t) => t.accessRequestId)), ); 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(); pendingTaskContractIds.forEach((contractId, idx) => { const contract = pendingTaskContracts[idx]; if (contract) { resultMap.set(contractId, contract); } }); return resultMap; } *fetchContractsForUser( token: string | undefined, ): GeneratorFn { try { const rawContracts = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsForUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject[]; 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> { try { const rawContracts = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsCreatedByUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject[]; const contracts = rawContracts.map((rawContract) => deserialize( V1_liteDataContractWithUserStatusModelSchema( this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ), rawContract, ), ); const resultMap = new Map(); 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 { try { const raw = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getDataAccessRequestsCreatedBy( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject; 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_LiteAccessRequest[], token: string | undefined, ): GeneratorFn> { const uniqueDIDToDataProduct = new Map(); for (const contract of allContracts) { uniqueDIDToDataProduct.set(contract.deploymentId, contract.resourceId); } const didToEnvType = new Map(); 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 getUnverifiedIngestDefinitions( contractId: string, token: string | undefined, ): Promise { const entitlementsStore = this.lakehouseEntitlementsStore; const baseStore = entitlementsStore.marketplaceBaseStore; const applicationStore = entitlementsStore.applicationStore; const plugins = applicationStore.pluginManager.getPureProtocolProcessorPlugins(); const contractClient = entitlementsStore.lakehouseContractServerClient; const PROD_ENV = 'prod'; const SDLC_DEPLOYMENT = 'alloy-git'; try { const liteContract = await (async () => { 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; } })(); if (!liteContract) { return []; } const accessPointGroupId = liteContract.resourceType === V1_ResourceType.ACCESS_POINT_GROUP ? (liteContract.accessPointGroup ?? undefined) : undefined; if (!accessPointGroupId) { return []; } const dpDetails = await (async () => { try { const raw = await contractClient.getDataProductByIdAndDID( liteContract.resourceId, liteContract.deploymentId, token, ); return V1_entitlementsDataProductDetailsResponseToDataProductDetails( raw, )[0]; } catch (error) { assertErrorThrown(error); return undefined; } })(); if (!dpDetails) { return []; } if (!(dpDetails.origin instanceof V1_SdlcDeploymentDataProductOrigin)) { return []; } const graphManager = new V1_PureGraphManager( applicationStore.pluginManager, applicationStore.logService, baseStore.remoteEngine, ); await graphManager.initialize( { env: applicationStore.config.env, tabSize: DEFAULT_TAB_SIZE, clientConfig: { baseUrl: applicationStore.config.engineServerUrl, }, }, { engine: baseStore.remoteEngine }, ); const v1DataProduct = await getDataProductFromDetails( dpDetails, graphManager, baseStore, ); if (!v1DataProduct) { return []; } const specs = collectIngestSpecPathsFromOriginDp( v1DataProduct, accessPointGroupId, graphManager, plugins, ); if (specs.size === 0) { return []; } const ingestEnvironment = await baseStore.lakehouseDataProductService.getOrFetchEnvironmentForDID( liteContract.deploymentId, token, ); const ingestServerUrl = ingestEnvironment?.ingestServerUrl; if (ingestServerUrl === undefined) { return []; } const sdlcOrigin = guaranteeType( dpDetails.origin, V1_SdlcDeploymentDataProductOrigin, ); const gav = `${sdlcOrigin.group}~${sdlcOrigin.artifact}`; const specsToVerify = Array.from(specs, (specPath) => ({ specPath, urn: `urn:lakehouse:${PROD_ENV}:ingest:definition:${SDLC_DEPLOYMENT}:${gav}~${specPath}`, })); const ingestClient = baseStore.lakehouseIngestServerClient; const settled = await Promise.all( specsToVerify.map(async (entry) => { try { await ingestClient.getIngestDefinitionDetail( entry.urn, ingestServerUrl, token, ); return undefined; } catch (error) { assertErrorThrown(error); if ( error instanceof NetworkClientError && error.response.status === HttpStatus.NOT_FOUND ) { return entry.specPath; } return undefined; } }), ); return settled.filter(isNonNullable); } catch (error) { assertErrorThrown(error); return []; } } async getContractSyncErrors( contractId: string, token: string | undefined, ): Promise { try { const response = (await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractSyncStatus( contractId, token, )) as LakehouseContractSyncStatusResponse; const status = response.status.toUpperCase(); if (status === ContractSyncStatus.NEVER_SYNCED) { return { title: 'Sync Error: Contract Never Synced' }; } if (status === ContractSyncStatus.NOT_FULLY_SYNCED) { const unsyncedUsers = response.unsyncedUsers?.map((user) => user.username) ?? []; const unsyncedAccessPoints = response.unsyncedAccessPoints?.map( (accessPoint) => accessPoint.accessPointName, ) ?? []; const unsyncedTargetAccounts = response.unsyncedTargetAccounts ?? []; const syncGroupingLayers: ContractErrorLayer[] = [ { title: 'Users:', errorItems: unsyncedUsers }, { title: 'Target Accounts:', errorItems: unsyncedTargetAccounts }, { title: 'Access Points:', errorItems: unsyncedAccessPoints }, ].filter((layer) => layer.errorItems.length > 0); if (syncGroupingLayers.length === 0) { return undefined; } return { title: 'Unsynced Entities:', childLayers: syncGroupingLayers, }; } return undefined; } catch (error) { assertErrorThrown(error); return undefined; } } async getContractErrors( contractId: string, token: string | undefined, checkSyncStatus = false, ): Promise { const [unverifiedIngestDefinitions, syncErrorsLayer] = await Promise.all([ this.getUnverifiedIngestDefinitions(contractId, token), checkSyncStatus ? this.getContractSyncErrors(contractId, token) : Promise.resolve(undefined), ]); const childLayers: ContractErrorLayer[] = [ unverifiedIngestDefinitions.length > 0 ? { title: `Ingest${unverifiedIngestDefinitions.length === 1 ? '' : 's'} Not Found:`, errorItems: unverifiedIngestDefinitions, } : undefined, syncErrorsLayer, ].filter(isNonNullable); return childLayers.length > 0 ? { title: 'Contract Errors:', childLayers } : undefined; } filterByUserEnvironment( pendingData: { tasks: V1_PendingTaskRecord[]; taskContractMap: Map; }, contractsForUser: V1_LiteDataContractWithUserStatus[], contractsCreatedByUserMap: Map, dataRequests: V1_DataRequestWithWorkflow[], envMap: Map, ): { filteredTasks: V1_PendingTaskRecord[]; filteredContractsForUser: V1_LiteDataContractWithUserStatus[]; filteredCreatedByUserMap: Map; 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.accessRequestId); 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 { 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); }); } private getDataRequestWorkflowId(dataRequestId: string): string { const detail = this.pendingDataRequestDetailsMap.get(dataRequestId); const workflowId = detail?.workflows[0]?.workflowId; if (!workflowId) { throw new Error(`No workflow found for data request ${dataRequestId}`); } return workflowId; } private *changeTaskStatus( task: V1_PendingTaskRecord, token: string | undefined, taskAction: TaskApprovalAction, justification: string | undefined, ): GeneratorFn { const isApprove = taskAction === TaskApprovalAction.APPROVE; if (task instanceof V1_DataRequestUserEventRecord) { const workflowId = this.getDataRequestWorkflowId(task.dataRequestId); const permitClient = this.lakehouseEntitlementsStore.marketplaceBaseStore .permitWorkflowServerClient; if (!permitClient) { throw new Error('Permit workflow client is not configured'); } yield permitClient.performTaskAction( workflowId, task.taskId, isApprove ? V1_PermitTaskAction.APPROVE : V1_PermitTaskAction.REJECT, justification, token, ); task.status = isApprove ? V1_UserApprovalStatus.APPROVED : V1_UserApprovalStatus.DENIED; } else { const contractClient = this.lakehouseEntitlementsStore.lakehouseContractServerClient; const response = (yield isApprove ? contractClient.approveTask(task.taskId, token, justification) : contractClient.denyTask( task.taskId, token, guaranteeNonNullable(justification), )) as PlainObject; const change = deserialize( V1_TaskStatusChangeResponseModelSchema, response, ); if (change.errorMessage) { throw new Error( `Unable to ${taskAction} task: ${task.taskId}: ${change.errorMessage}`, ); } task.status = change.status; } this.pendingTasks = [...(this.pendingTasks ?? [])]; this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.invalidate(); } *approve( task: V1_PendingTaskRecord, token: string | undefined, justification: string | undefined, ): GeneratorFn { try { this.changingState.inProgress(); this.changingState.setMessage('Approving Task'); yield* this.changeTaskStatus( task, token, TaskApprovalAction.APPROVE, justification, ); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess( `Task has been Approved`, ); } finally { this.changingState.complete(); this.changingState.setMessage(undefined); } } *deny( task: V1_PendingTaskRecord, token: string | undefined, justification: string, ): GeneratorFn { try { this.changingState.inProgress(); this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert( { message: 'Denying Task', prompt: 'Denying task...', showLoading: true, }, ); yield* this.changeTaskStatus( task, token, TaskApprovalAction.DENY, justification, ); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess( `Task has been denied`, ); } finally { this.changingState.complete(); this.changingState.setMessage(undefined); this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert( undefined, ); } } }