Skip to content

Commit

Permalink
feat: add EntityEdgeDeletionPermissionInferenceBehavior for canViewer…
Browse files Browse the repository at this point in the history
…DeleteAsync
  • Loading branch information
wschurman committed Jun 24, 2024
1 parent 43e7c9c commit 90fd858
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 12 deletions.
10 changes: 10 additions & 0 deletions packages/entity/src/AuthorizationResultBasedEntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ export default class AuthorizationResultBasedEntityLoader<
return entityResultsForFieldValue!;
}

/**
*
*/
async loadOneOfManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>,
): Promise<Result<TEntity> | null> {
return null;
}

/**
* Load an entity where fieldName equals fieldValue, or null if no entity exists.
* @param uniqueFieldName - entity field being queried
Expand Down
12 changes: 12 additions & 0 deletions packages/entity/src/EntityFieldDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export enum EntityEdgeDeletionBehavior {
SET_NULL,
}

export enum EntityEdgeDeletionPermissionInferenceBehavior {
ALLOW_SINGLE_ENTITY_INDUCTION,
}

/**
* Defines an association between entities. An association is primarily used to define cascading deletion behavior.
*/
Expand Down Expand Up @@ -93,6 +97,14 @@ export interface EntityAssociationDefinition<
* integrity is recommended.
*/
edgeDeletionBehavior: EntityEdgeDeletionBehavior;

/**
* Edge deletion behavior cascading permission inferrence.
*
* Used for canViewerDeleteAsync to optimize permission checks for edge cascading deletions (and set nulls).
* Not yet used for actual deletions as a safety precaution.
*/
edgeDeletionPermissionInferenceBehavior?: EntityEdgeDeletionPermissionInferenceBehavior;
}

/**
Expand Down
52 changes: 40 additions & 12 deletions packages/entity/src/utils/EntityPrivacyUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { asyncResult } from '@expo/results';
import { Result, asyncResult } from '@expo/results';

import Entity, { IEntityClass } from '../Entity';
import { EntityEdgeDeletionBehavior } from '../EntityFieldDefinition';
import {
EntityEdgeDeletionBehavior,
EntityEdgeDeletionPermissionInferenceBehavior,
} from '../EntityFieldDefinition';
import { EntityCascadingDeletionInfo } from '../EntityMutationInfo';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
Expand Down Expand Up @@ -254,16 +257,39 @@ async function canViewerDeleteInternalAsync<
continue;
}

const entityResultsForInboundEdge = await loader
.withAuthorizationResults()
.loadManyByFieldEqualingAsync(
fieldName,
association.associatedEntityLookupByField
? sourceEntity.getField(association.associatedEntityLookupByField as any)
: sourceEntity.getID(),
);
const edgeDeletionPermissionInferenceBehavior =
association.edgeDeletionPermissionInferenceBehavior;

const failedEntityLoadResults = failedResults(entityResultsForInboundEdge);
let entityResultsToCheckForInboundEdge: readonly Result<any>[];

if (
edgeDeletionPermissionInferenceBehavior ===
EntityEdgeDeletionPermissionInferenceBehavior.ALLOW_SINGLE_ENTITY_INDUCTION
) {
const singleEntityToTestForInboundEdge = await loader
.withAuthorizationResults()
.loadFirstByFieldEqualingAsync(
fieldName,
association.associatedEntityLookupByField
? sourceEntity.getField(association.associatedEntityLookupByField as any)
: sourceEntity.getID(),
);
entityResultsToCheckForInboundEdge = singleEntityToTestForInboundEdge
? [singleEntityToTestForInboundEdge]
: [];
} else {
const entityResultsForInboundEdge = await loader
.withAuthorizationResults()
.loadManyByFieldEqualingAsync(
fieldName,
association.associatedEntityLookupByField
? sourceEntity.getField(association.associatedEntityLookupByField as any)
: sourceEntity.getID(),
);
entityResultsToCheckForInboundEdge = entityResultsForInboundEdge;
}

const failedEntityLoadResults = failedResults(entityResultsToCheckForInboundEdge);
for (const failedResult of failedEntityLoadResults) {
if (failedResult.reason instanceof EntityNotAuthorizedError) {
return false;
Expand All @@ -273,7 +299,9 @@ async function canViewerDeleteInternalAsync<
}

// all results should be success at this point due to check above
const entitiesForInboundEdge = entityResultsForInboundEdge.map((r) => r.enforceValue());
const entitiesForInboundEdge = entityResultsToCheckForInboundEdge.map((r) =>
r.enforceValue(),
);

switch (association.edgeDeletionBehavior) {
case EntityEdgeDeletionBehavior.CASCADE_DELETE:
Expand Down

0 comments on commit 90fd858

Please sign in to comment.