Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,27 @@ export interface AllowIfInParentCascadeDeletionDirective<
TParentSelectedFields extends keyof TParentFields = keyof TParentFields,
> {
/**
* Class of parent entity that should trigger a cascade set null update to a field within
* the entity being authorized.
* Class (or classes) of parent entity that should trigger a cascade set null update to a field
* within the entity being authorized. Pass an array when multiple sibling classes backed by the
* same table can be the deletion source.
*/
parentEntityClass: IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>;
parentEntityClass:
| IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>
| readonly IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>[];

/**
* Field of the current entity with references the deleting instace of parentEntityClass.
Expand Down Expand Up @@ -138,10 +148,12 @@ export class AllowIfInParentCascadeDeletionPrivacyPolicyRule<
>,
entity: TEntity,
): Promise<RuleEvaluationResult> {
const parentEntityClass = this.directive.parentEntityClass;
const parentEntityClasses = Array.isArray(this.directive.parentEntityClass)
? this.directive.parentEntityClass
: [this.directive.parentEntityClass];

const deleteCause = evaluationContext.cascadingDeleteCause;
if (!deleteCause || !(deleteCause.entity instanceof parentEntityClass)) {
if (!deleteCause || !parentEntityClasses.some((cls) => deleteCause.entity instanceof cls)) {
return RuleEvaluationResult.SKIP;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,108 @@ describePrivacyPolicyRule(
},
);

// Test with array of parent entity classes
class TestSiblingParentEntity extends ReadonlyEntity<ParentFields, 'id', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
ParentFields,
'id',
ViewerContext,
TestSiblingParentEntity,
TestParentPrivacyPolicy
> {
throw new Error('Not implemented for test');
}
}

const siblingParentEntityMock = mock(TestSiblingParentEntity);
when(siblingParentEntityMock.getID()).thenReturn('5');
const siblingParentEntity = instance(siblingParentEntityMock);
Object.setPrototypeOf(siblingParentEntity, TestSiblingParentEntity.prototype);

const childEntityForSiblingMock = mock(TestChildEntity);
when(childEntityForSiblingMock.getField('parent_id')).thenReturn('5');

const childEntityForSiblingNullifiedMock = mock(TestChildEntity);
when(childEntityForSiblingNullifiedMock.getField('parent_id')).thenReturn(null);

describePrivacyPolicyRule(
new AllowIfInParentCascadeDeletionPrivacyPolicyRule<
ChildFields,
'id',
ViewerContext,
TestChildEntity,
ParentFields,
'id',
TestParentEntity,
TestParentPrivacyPolicy
>({
fieldIdentifyingParentEntity: 'parent_id',
parentEntityClass: [TestParentEntity, TestSiblingParentEntity],
}),
{
allowCases: [
// first class in array: parent id matches, field not yet nullified
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityMock),
cascadingDeleteCause: {
entity: parentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityMock),
},
// second class in array: sibling parent id matches, field not yet nullified
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityForSiblingMock),
cascadingDeleteCause: {
entity: siblingParentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityForSiblingMock),
},
// second class in array: field nullified, previous value matches
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityForSiblingMock),
cascadingDeleteCause: {
entity: siblingParentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityForSiblingNullifiedMock),
},
],
skipCases: [
// unrelated entity class not in array
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: null,
cascadingDeleteCause: {
entity: instance(unrelatedOtherEntityMock),
cascadingDeleteCause: null,
},
},
entity: instance(childEntityMock),
},
],
},
);

// Test with custom lookup field (parentEntityLookupByField)
const parentEntityWithNameMock = mock(TestParentEntity);
when(parentEntityWithNameMock.getField('name')).thenReturn('test-name');
Expand Down