Skip to content

Commit

Permalink
Merge pull request #2659 from cloudfoundry-incubator/remove-partial-e…
Browse files Browse the repository at this point in the history
…ntity-2

Update parent entities at validation time - the end of partial entities
  • Loading branch information
KlapTrap authored Jul 16, 2018
2 parents 5a226fb + d4ee61f commit ef001fb
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 117 deletions.
5 changes: 0 additions & 5 deletions src/frontend/app/core/entity-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ export class EntityService<T = any> {
if (!validateRelations || validated || isEntityBlocked(entityInfo.entityRequestInfo)) {
return;
}
// If we're not an 'official' object, go forth and fetch again. This will populate all the required '<entity>__guid' fields.
if (!entityInfo.entity.metadata) {
this.actionDispatch();
return;
}
validated = true;
store.dispatch(new ValidateEntitiesStart(
action as ICFAction,
Expand Down
18 changes: 11 additions & 7 deletions src/frontend/app/store/effects/request.effects.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

import { catchError, first, map, mergeMap, withLatestFrom } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { RequestMethod } from '@angular/http';
import { Actions, Effect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { catchError, first, map, mergeMap, withLatestFrom } from 'rxjs/operators';

import { LoggerService } from '../../core/logger.service';
import { UtilsService } from '../../core/utils.service';
Expand All @@ -16,6 +15,7 @@ import {
} from '../actions/request.actions';
import { AppState } from '../app-state';
import { validateEntityRelations } from '../helpers/entity-relations';
import { ValidationResult } from '../helpers/entity-relations.types';
import { getRequestTypeFromMethod } from '../reducers/api-request-reducer/request-helpers';
import { rootUpdatingKey } from '../reducers/api-request-reducer/types';
import { getAPIRequestDataState } from '../selectors/api.selectors';
Expand All @@ -27,6 +27,7 @@ import {
WrapperRequestActionSuccess,
} from '../types/request.types';


@Injectable()
export class RequestEffect {
constructor(
Expand Down Expand Up @@ -80,16 +81,19 @@ export class RequestEffect {
return this.store.select(getAPIRequestDataState).pipe(
withLatestFrom(this.store.select(getPaginationState)),
first(),
map(([allEntities, allPagination]) => {
map(([allEntities, allPagination]): ValidationResult => {
// The apiResponse will be null if we're validating as part of the entity service, not during an api request
const entities = apiResponse ? apiResponse.response.entities : null;
return apiAction.skipValidation ? {
started: false,
completed: Promise.resolve([])
completed: Promise.resolve([]),
apiResponse
} : validateEntityRelations({
cfGuid: validateAction.action.endpointGuid,
store: this.store,
allEntities,
allPagination,
newEntities: apiResponse ? apiResponse.response.entities : null,
apiResponse,
action: validateAction.action,
parentEntities: validateAction.validateEntities,
populateMissing: true,
Expand All @@ -108,7 +112,7 @@ export class RequestEffect {
mergeMap(({ independentUpdates, validation }) => {
return [new EntitiesPipelineCompleted(
apiAction,
apiResponse,
validation.apiResponse,
validateAction,
validation,
independentUpdates
Expand Down Expand Up @@ -162,7 +166,7 @@ export class RequestEffect {

if (
!apiAction.updatingKey &&
( apiAction.options.method === 'post' || apiAction.options.method === RequestMethod.Post ||
(apiAction.options.method === 'post' || apiAction.options.method === RequestMethod.Post ||
apiAction.options.method === 'delete' || apiAction.options.method === RequestMethod.Delete)
) {
if (apiAction.removeEntityOnDelete) {
Expand Down
147 changes: 105 additions & 42 deletions src/frontend/app/store/helpers/entity-relations.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@

import { of as observableOf, Observable } from 'rxjs';
import { Action, Store } from '@ngrx/store';
import { denormalize } from 'normalizr';
import { filter, first, map, mergeMap, pairwise, skipWhile, withLatestFrom } from 'rxjs/operators';
import { Observable, of as observableOf } from 'rxjs';
import { filter, first, map, mergeMap, pairwise, skipWhile, switchMap, withLatestFrom } from 'rxjs/operators';

import { isEntityBlocked } from '../../core/entity-service';
import { pathGet } from '../../core/utils.service';
import { pathGet, pathSet } from '../../core/utils.service';
import { SetInitialParams } from '../actions/pagination.actions';
import { FetchRelationPaginatedAction, FetchRelationSingleAction } from '../actions/relation.actions';
import { FetchRelationAction, FetchRelationPaginatedAction, FetchRelationSingleAction } from '../actions/relation.actions';
import { APIResponse } from '../actions/request.actions';
import { AppState } from '../app-state';
import { ActionState, RequestInfoState } from '../reducers/api-request-reducer/types';
import { getAPIRequestDataState, selectEntity, selectRequestInfo } from '../selectors/api.selectors';
import { selectPaginationState } from '../selectors/pagination.selectors';
import { APIResource, NormalizedResponse } from '../types/api.types';
import { PaginatedAction, PaginationEntityState } from '../types/pagination.types';
import { IRequestAction, UpdateCfAction, WrapperRequestActionSuccess } from '../types/request.types';
import { IRequestAction, RequestEntityLocation, WrapperRequestActionSuccess } from '../types/request.types';
import { EntitySchema } from './entity-factory';
import { fetchEntityTree } from './entity-relations.tree';
import {
Expand All @@ -27,6 +27,7 @@ import {
} from './entity-relations.types';
import { pick } from './reducer.helper';


class AppStoreLayout {
[entityKey: string]: {
[guid: string]: any;
Expand All @@ -43,8 +44,9 @@ interface ValidateResultFetchingState {
* @interface ValidateEntityResult
*/
interface ValidateEntityResult {
action: Action;
action: FetchRelationAction;
fetchingState$?: Observable<ValidateResultFetchingState>;
abortDispatch?: boolean;
}

class ValidateEntityRelationsConfig {
Expand Down Expand Up @@ -78,7 +80,7 @@ class ValidateEntityRelationsConfig {
* @type {AppStoreLayout}
* @memberof ValidateEntityRelationsConfig
*/
newEntities: AppStoreLayout;
newEntities?: AppStoreLayout;
/**
* The action that has fetched the entity/entities
*
Expand All @@ -99,6 +101,12 @@ class ValidateEntityRelationsConfig {
* @memberof ValidateEntityRelationsConfig
*/
populateMissing = true;
/**
* If we're validating an api request we'll have the apiResponse, otherwise it's null and we're ad hoc validating an entity/list
*
* @memberof ValidateEntityRelationsConfig
*/
apiResponse: APIResponse;
}

class ValidateLoopConfig extends ValidateEntityRelationsConfig {
Expand Down Expand Up @@ -190,7 +198,7 @@ function createEntityWatcher(store, paramAction, guid: string): Observable<Valid
* @param {HandleRelationsConfig} config
* @returns {ValidateEntityResult[]}
*/
function createActionsForExistingEntities(config: HandleRelationsConfig): ValidateEntityResult {
function createActionsForExistingEntities(config: HandleRelationsConfig): Action {
const { store, allEntities, newEntities, childEntities, childRelation } = config;
const childEntitiesAsArray = childEntities as Array<any>;

Expand All @@ -208,17 +216,13 @@ function createActionsForExistingEntities(config: HandleRelationsConfig): Valida
result: guids
};

const action = new WrapperRequestActionSuccess(
return new WrapperRequestActionSuccess(
response,
paramAction,
'fetch',
childEntitiesAsArray.length,
1
);
return {
action,
fetchingState$: childRelation.isArray ? createEntityWatcher(store, paramAction, guids[0]) : null
};
}

/**
Expand Down Expand Up @@ -281,19 +285,12 @@ function handleRelation(config: HandleRelationsConfig): ValidateEntityResult[] {
let results = [];
if (childEntities) {
if (!childRelation.isArray) {
// We've already got the missing entity in the store, we just need to associate it with it's parent. We can do this via two actions
// 1) a pretend 'request finished' event which handles the request data side of things
const connectEntityWithParent = createActionsForExistingEntities(config);
// 2) a pretend update which changes the entity's request info state in order for the entity service to emit the updated entity.
// This won't be fired often (missing single entities are rare, as opposed to missing lists of entities + also should only occur once
// per entity)
const notifyParentListenersOfChange: ValidateEntityResult = {
action: new UpdateCfAction({
...config.action,
updatingKey: `AssociatedChildAt:${new Date()}`
}, false, '')
// We've already got the missing entity in the store or current response, we just need to associate it with it's parent
const connectEntityWithParent: ValidateEntityResult = {
action: createSingleAction(config),
abortDispatch: true // Don't need to make the request.. it's either in the store or as part of apiResource with be
};
results = [].concat(results, connectEntityWithParent, notifyParentListenersOfChange);
results = [].concat(results, connectEntityWithParent);
}
} else {
if (populateMissing) {
Expand Down Expand Up @@ -380,27 +377,91 @@ function validationLoop(config: ValidateLoopConfig): ValidateEntityResult[] {
return results;
}

function handleValidationLoopResults(store, results) {
function associateChildWithParent(store, action: FetchRelationAction, apiResponse: APIResponse): Observable<boolean> {
let childValue;
// Fetch the child value to associate with parent. Will either be a guid or a list of guids
if (action.child.isArray) {
const paginationAction = action as FetchRelationPaginatedAction;
childValue = store.select(selectPaginationState(action.entityKey, paginationAction.paginationKey)).pipe(
first(),
map((paginationSate: PaginationEntityState) => paginationSate.ids[1] || [])
);
} else {
const entityAction = action as FetchRelationSingleAction;
childValue = observableOf(entityAction.guid);
}

return childValue.pipe(
map(value => {
if (!value) {
return true;
}

if (apiResponse) {
// Part of an api call. Assign to apiResponse which is added to store later
apiResponse.response.entities[action.parentEntitySchema.key][action.parentGuid].entity[action.child.paramName] = value;
} else {
// Not part of an api call. We already have the entity in the store, so fire off event to link child with parent
const response = {
entities: {
[action.parentEntitySchema.key]: {
[action.parentGuid]: {
entity: {
[action.child.paramName]: value
}
}
}
},
result: [action.parentGuid]
};
const parentAction: IRequestAction = {
endpointGuid: action.endpointGuid,
entity: action.parentEntitySchema,
entityLocation: RequestEntityLocation.OBJECT,
guid: action.parentGuid,
entityKey: action.parentEntitySchema.key,
type: '[Entity] Associate with parent',
};
// Add for easier debugging
parentAction['childEntityKey'] = action.child.entityKey;

const successAction = new WrapperRequestActionSuccess(response, parentAction, 'fetch', 1, 1);
store.dispatch(successAction);
}
return true;
})
);
}

function handleValidationLoopResults(store: Store<AppState>, results: ValidateEntityResult[], apiResponse: APIResponse): ValidationResult {
const paginationFinished = new Array<Promise<boolean>>();

results.forEach(newActions => {
store.dispatch(newActions.action);
if (newActions.fetchingState$) {
const obs = newActions.fetchingState$.pipe(
pairwise(),
map(([oldFetching, newFetching]) => {
return oldFetching.fetching === true && newFetching.fetching === false;
}),
skipWhile(completed => !completed),
first(),
).toPromise();
paginationFinished.push(obs);
results.forEach(request => {
// Fetch any missing data
if (!request.abortDispatch) {
store.dispatch(request.action);
}
// Wait for the action to be completed
const obs = request.fetchingState$ ? request.fetchingState$.pipe(
pairwise(),
map(([oldFetching, newFetching]) => {
return oldFetching.fetching === true && newFetching.fetching === false;
}),
skipWhile(completed => !completed),
first()) : observableOf(true);
// Associate the missing parameter with it's parent
const associatedObs = obs.pipe(
switchMap(() => {
const action: FetchRelationAction = FetchRelationAction.is(request.action);
return action ? associateChildWithParent(store, action, apiResponse) : observableOf(true);
}),
).toPromise();
paginationFinished.push(associatedObs);
});

return {
started: !!(paginationFinished.length),
completed: Promise.all(paginationFinished)
completed: Promise.all(paginationFinished),
apiResponse
};
}

Expand All @@ -416,6 +477,7 @@ function handleValidationLoopResults(store, results) {
* @returns {ValidationResult}
*/
export function validateEntityRelations(config: ValidateEntityRelationsConfig): ValidationResult {
config.newEntities = config.apiResponse ? config.apiResponse.response.entities : null;
const { action, populateMissing, newEntities, allEntities, store } = config;
let { parentEntities } = config;

Expand All @@ -441,7 +503,7 @@ export function validateEntityRelations(config: ValidateEntityRelationsConfig):
parentRelation: entityTree.rootRelation,
});

return handleValidationLoopResults(store, results);
return handleValidationLoopResults(store, results, config.apiResponse);
}

export function listEntityRelations(action: EntityInlineParentAction) {
Expand Down Expand Up @@ -513,6 +575,7 @@ export function populatePaginationFromParent(store: Store<AppState>, action: Pag
allEntities,
allPagination: {},
newEntities: {},
apiResponse: null,
parentEntities: null,
entities: entity.entity[paramName],
childEntities: entity.entity[paramName],
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/app/store/helpers/entity-relations.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Action } from '@ngrx/store';

import { getPaginationKey } from '../actions/pagination.actions';
import { APIResponse } from '../actions/request.actions';
import { IRequestAction } from '../types/request.types';
import { EntitySchema } from './entity-factory';

Expand Down Expand Up @@ -107,4 +108,13 @@ export class ValidationResult {
* @memberof ValidationResult
*/
completed: Promise<boolean[]>;

/**
* The new apiResponse. For the case of validating api calls this might be updated to ensure parent entities are associated with missing
* children.
*
* @type {APIResponse}
* @memberof ValidationResult
*/
apiResponse?: APIResponse;
}
Loading

0 comments on commit ef001fb

Please sign in to comment.