Skip to content

Commit

Permalink
feat: make canViewerDeleteAsync recursive (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman authored May 31, 2024
1 parent 14bd959 commit 60fc9a4
Show file tree
Hide file tree
Showing 7 changed files with 947 additions and 350 deletions.
117 changes: 1 addition & 116 deletions packages/entity/src/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Result, asyncResult } from '@expo/results';
import { Result } from '@expo/results';

import { EntityCompanionDefinition } from './EntityCompanionProvider';
import { CreateMutator, UpdateMutator } from './EntityMutator';
Expand Down Expand Up @@ -197,121 +197,6 @@ export default abstract class Entity<
.forDelete(existingEntity, queryContext)
.enforceDeleteAsync();
}

/**
* Check whether an entity loaded by a viewer can be updated by that same viewer.
*
* @remarks
*
* This may be useful in situations relying upon the thrown privacy policy thrown authorization error
* is insufficient for the task at hand. When dealing with purely a sequence of mutations it is easy
* to roll back all mutations given a single authorization error by wrapping them in a single transaction.
* When certain portions of a mutation cannot be rolled back transactionally (third pary calls,
* legacy code, etc), using this method can help decide whether the sequence of mutations will fail before
* attempting them. Note that if any privacy policy rules use a piece of data being updated in the mutations
* the result of this method and the update mutation itself may differ.
*
* @param existingEntity - entity loaded by viewer
* @param queryContext - query context in which to perform the check
*/
static async canViewerUpdateAsync<
TMFields extends object,
TMID extends NonNullable<TMFields[TMSelectedFields]>,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<boolean> {
const companion = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this);
const privacyPolicy = companion.entityCompanion.privacyPolicy;
const evaluationResult = await asyncResult(
privacyPolicy.authorizeUpdateAsync(
existingEntity.getViewerContext(),
queryContext,
{ cascadingDeleteCause: null },
existingEntity,
companion.getMetricsAdapter()
)
);
return evaluationResult.ok;
}

/**
* Check whether an entity loaded by a viewer can be deleted by that same viewer.
*
* @remarks
* See remarks for canViewerUpdate.
*
* @param existingEntity - entity loaded by viewer
* @param queryContext - query context in which to perform the check
*/
static async canViewerDeleteAsync<
TMFields extends object,
TMID extends NonNullable<TMFields[TMSelectedFields]>,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<boolean> {
const companion = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this);
const privacyPolicy = companion.entityCompanion.privacyPolicy;
const evaluationResult = await asyncResult(
privacyPolicy.authorizeDeleteAsync(
existingEntity.getViewerContext(),
queryContext,
{ cascadingDeleteCause: null },
existingEntity,
companion.getMetricsAdapter()
)
);
return evaluationResult.ok;
}
}

/**
Expand Down
56 changes: 24 additions & 32 deletions packages/entity/src/EntityMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import EntityMutationTriggerConfiguration, {
import EntityMutationValidator from './EntityMutationValidator';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import { EntityQueryContext, EntityTransactionalQueryContext } from './EntityQueryContext';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import EntityInvalidFieldValueError from './errors/EntityInvalidFieldValueError';
import { timeAndLogMutationEventAsync } from './metrics/EntityMetricsUtils';
Expand Down Expand Up @@ -741,8 +740,23 @@ export class DeleteMutator<
).entityCompanionDefinition;
const entityConfiguration = companionDefinition.entityConfiguration;
const inboundEdges = entityConfiguration.inboundEdges;

const newCascadingDeleteCause = {
entity,
cascadingDeleteCause,
};

await Promise.all(
inboundEdges.map(async (entityClass) => {
const loaderFactory = entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(entityClass)
.getLoaderFactory();
const mutatorFactory = entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(entityClass)
.getMutatorFactory();

return await mapMapAsync(
this.companionProvider.getCompanionForEntity(entityClass).entityCompanionDefinition
.entityConfiguration.schema,
Expand All @@ -759,37 +773,15 @@ export class DeleteMutator<
return;
}

const associatedEntityLookupByField = association.associatedEntityLookupByField;

const loaderFactory = entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(entityClass)
.getLoaderFactory();
const mutatorFactory = entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(entityClass)
.getMutatorFactory();

const newCascadingDeleteCause = {
entity,
cascadingDeleteCause,
};

let inboundReferenceEntities: readonly ReadonlyEntity<any, any, any, any>[];
if (associatedEntityLookupByField) {
inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.enforcing()
.loadManyByFieldEqualingAsync(
fieldName,
entity.getField(associatedEntityLookupByField as any)
);
} else {
inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.enforcing()
.loadManyByFieldEqualingAsync(fieldName, entity.getID());
}
const inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.enforcing()
.loadManyByFieldEqualingAsync(
fieldName,
association.associatedEntityLookupByField
? entity.getField(association.associatedEntityLookupByField as any)
: entity.getID()
);

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

0 comments on commit 60fc9a4

Please sign in to comment.