diff --git a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts index 42ea1a65..19ee1128 100644 --- a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts +++ b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts @@ -105,6 +105,16 @@ export default class AuthorizationResultBasedEntityLoader< return entityResultsForFieldValue!; } + /** + * + */ + async loadOneOfManyByFieldEqualingAsync>( + fieldName: N, + fieldValue: NonNullable, + ): Promise | null> { + return null; + } + /** * Load an entity where fieldName equals fieldValue, or null if no entity exists. * @param uniqueFieldName - entity field being queried diff --git a/packages/entity/src/EntityFieldDefinition.ts b/packages/entity/src/EntityFieldDefinition.ts index 74dbcf2c..f9aea27c 100644 --- a/packages/entity/src/EntityFieldDefinition.ts +++ b/packages/entity/src/EntityFieldDefinition.ts @@ -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. */ @@ -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; } /** diff --git a/packages/entity/src/utils/EntityPrivacyUtils.ts b/packages/entity/src/utils/EntityPrivacyUtils.ts index 4155acd3..c4684cb5 100644 --- a/packages/entity/src/utils/EntityPrivacyUtils.ts +++ b/packages/entity/src/utils/EntityPrivacyUtils.ts @@ -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'; @@ -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[]; + + if ( + edgeDeletionPermissionInferenceBehavior === + EntityEdgeDeletionPermissionInferenceBehavior.ALLOW_SINGLE_ENTITY_INDUCTION + ) { + const singleEntityToTestForInboundEdge = await loader + .withAuthorizationResults() + .loadOneOfManyByFieldEqualingAsync( + 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; @@ -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: