@expo/entity 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/ComposedEntityCacheAdapter.js.map +1 -1
- package/build/src/ComposedSecondaryEntityCache.js.map +1 -1
- package/build/src/EntityDatabaseAdapter.js.map +1 -1
- package/build/src/EntityFieldDefinition.js.map +1 -1
- package/build/src/EntityLoaderUtils.js +14 -18
- package/build/src/EntityLoaderUtils.js.map +1 -1
- package/build/src/GenericEntityCacheAdapter.js.map +1 -1
- package/build/src/GenericSecondaryEntityCache.js.map +1 -1
- package/build/src/entityUtils.js +7 -2
- package/build/src/entityUtils.js.map +1 -1
- package/build/src/errors/EntityCacheAdapterError.d.ts +2 -2
- package/build/src/errors/EntityCacheAdapterError.js +12 -2
- package/build/src/errors/EntityCacheAdapterError.js.map +1 -1
- package/build/src/errors/EntityDatabaseAdapterError.d.ts +24 -24
- package/build/src/errors/EntityDatabaseAdapterError.js +111 -24
- package/build/src/errors/EntityDatabaseAdapterError.js.map +1 -1
- package/build/src/errors/EntityError.d.ts +2 -4
- package/build/src/errors/EntityError.js +5 -8
- package/build/src/errors/EntityError.js.map +1 -1
- package/build/src/errors/EntityInvalidFieldValueError.d.ts +2 -2
- package/build/src/errors/EntityInvalidFieldValueError.js +9 -2
- package/build/src/errors/EntityInvalidFieldValueError.js.map +1 -1
- package/build/src/errors/EntityNotAuthorizedError.d.ts +2 -2
- package/build/src/errors/EntityNotAuthorizedError.js +9 -2
- package/build/src/errors/EntityNotAuthorizedError.js.map +1 -1
- package/build/src/errors/EntityNotFoundError.d.ts +2 -2
- package/build/src/errors/EntityNotFoundError.js +9 -2
- package/build/src/errors/EntityNotFoundError.js.map +1 -1
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +4 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/CompositeFieldHolder.js.map +1 -1
- package/build/src/internal/CompositeFieldValueMap.js.map +1 -1
- package/build/src/internal/SingleFieldHolder.js.map +1 -1
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.d.ts +10 -0
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js +19 -0
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.d.ts +10 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js +19 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.d.ts +66 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js +75 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.d.ts +12 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js +23 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js.map +1 -0
- package/build/src/utils/EntityPrivacyUtils.js +34 -15
- package/build/src/utils/EntityPrivacyUtils.js.map +1 -1
- package/package.json +10 -13
- package/src/ComposedEntityCacheAdapter.ts +1 -2
- package/src/ComposedSecondaryEntityCache.ts +4 -3
- package/src/EntityDatabaseAdapter.ts +3 -2
- package/src/EntityFieldDefinition.ts +1 -2
- package/src/EntityLoaderUtils.ts +17 -20
- package/src/GenericEntityCacheAdapter.ts +1 -2
- package/src/GenericSecondaryEntityCache.ts +1 -2
- package/src/__tests__/ComposedCacheAdapter-test.ts +4 -3
- package/src/__tests__/EntityFields-test.ts +2 -8
- package/src/entityUtils.ts +7 -4
- package/src/errors/EntityCacheAdapterError.ts +16 -3
- package/src/errors/EntityDatabaseAdapterError.ts +137 -25
- package/src/errors/EntityError.ts +7 -8
- package/src/errors/EntityInvalidFieldValueError.ts +11 -2
- package/src/errors/EntityNotAuthorizedError.ts +11 -2
- package/src/errors/EntityNotFoundError.ts +11 -2
- package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +68 -11
- package/src/errors/__tests__/EntityError-test.ts +36 -0
- package/src/index.ts +4 -0
- package/src/internal/CompositeFieldHolder.ts +7 -10
- package/src/internal/CompositeFieldValueMap.ts +1 -2
- package/src/internal/SingleFieldHolder.ts +10 -6
- package/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts +47 -0
- package/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts +47 -0
- package/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.ts +177 -0
- package/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts +46 -0
- package/src/rules/__tests__/AllowIfAllSubRulesAllowPrivacyPolicyRule-test.ts +64 -0
- package/src/rules/__tests__/AllowIfAnySubRuleAllowsPrivacyPolicyRule-test.ts +64 -0
- package/src/rules/__tests__/AllowIfInParentCascadeDeletionPrivacyPolicyRule-test.ts +258 -0
- package/src/rules/__tests__/EvaluateIfEntityFieldPredicatePrivacyPolicyRule-test.ts +47 -0
- package/src/utils/EntityPrivacyUtils.ts +61 -20
- package/src/utils/__testfixtures__/StubCacheAdapter.ts +2 -4
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +1 -1
- package/src/utils/__tests__/EntityPrivacyUtils-test.ts +295 -0
|
@@ -268,6 +268,47 @@ describe(canViewerDeleteAsync, () => {
|
|
|
268
268
|
);
|
|
269
269
|
expect(canViewerDeleteResult.allowed).toBe(true);
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
it('evaluates privacy policy with synthetically nullified field for SET_NULL', async () => {
|
|
273
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
274
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
275
|
+
const testEntity = await ParentEntity.creator(viewerContext).createAsync();
|
|
276
|
+
|
|
277
|
+
// Create a leaf entity that references the parent. This leaf entity's privacy policy
|
|
278
|
+
// allows updates only when its reference field is being set to null. This tests that canViewerDeleteAsync
|
|
279
|
+
// creates a synthetic entity with the field set to null when evaluating the SET_NULL case.
|
|
280
|
+
await LeafConditionalUpdateEntity.creator(viewerContext)
|
|
281
|
+
.setField('parent_id', testEntity.getID())
|
|
282
|
+
.createAsync();
|
|
283
|
+
|
|
284
|
+
const canViewerDelete = await canViewerDeleteAsync(ParentEntity, testEntity);
|
|
285
|
+
expect(canViewerDelete).toBe(true);
|
|
286
|
+
|
|
287
|
+
const canViewerDeleteResult = await getCanViewerDeleteResultAsync(ParentEntity, testEntity);
|
|
288
|
+
expect(canViewerDeleteResult.allowed).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('denies deletion when privacy policy fails with synthetically nullified field for SET_NULL', async () => {
|
|
292
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
293
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
294
|
+
const testEntity = await ParentEntity.creator(viewerContext).createAsync();
|
|
295
|
+
|
|
296
|
+
// Create a leaf entity that references the parent. This leaf entity's privacy policy
|
|
297
|
+
// denies updates when its reference field is being set to null. This tests that canViewerDeleteAsync
|
|
298
|
+
// properly evaluates the synthetic entity and denies when the policy fails.
|
|
299
|
+
const leafEntity = await LeafDenyUpdateWhenNullEntity.creator(viewerContext)
|
|
300
|
+
.setField('parent_id', testEntity.getID())
|
|
301
|
+
.createAsync();
|
|
302
|
+
|
|
303
|
+
const canViewerDelete = await canViewerDeleteAsync(ParentEntity, testEntity);
|
|
304
|
+
expect(canViewerDelete).toBe(false);
|
|
305
|
+
|
|
306
|
+
const canViewerDeleteResult = await getCanViewerDeleteResultAsync(ParentEntity, testEntity);
|
|
307
|
+
expectAuthorizationError(canViewerDeleteResult, {
|
|
308
|
+
entityId: leafEntity.getID(),
|
|
309
|
+
action: EntityAuthorizationAction.UPDATE,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
271
312
|
});
|
|
272
313
|
|
|
273
314
|
type TestEntityFields = {
|
|
@@ -297,6 +338,20 @@ type TestEntityThrowOtherErrorFields = {
|
|
|
297
338
|
simple_test_id: string | null;
|
|
298
339
|
};
|
|
299
340
|
|
|
341
|
+
type ParentEntityFields = {
|
|
342
|
+
id: string;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
type LeafConditionalUpdateEntityFields = {
|
|
346
|
+
id: string;
|
|
347
|
+
parent_id: string | null;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
type LeafDenyUpdateWhenNullEntityFields = {
|
|
351
|
+
id: string;
|
|
352
|
+
parent_id: string | null;
|
|
353
|
+
};
|
|
354
|
+
|
|
300
355
|
class DenyUpdateEntityPrivacyPolicy<
|
|
301
356
|
TFields extends Record<'id', any>,
|
|
302
357
|
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
@@ -678,3 +733,243 @@ class SimpleTestThrowOtherErrorEntity extends Entity<
|
|
|
678
733
|
};
|
|
679
734
|
}
|
|
680
735
|
}
|
|
736
|
+
|
|
737
|
+
class ConditionalUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
738
|
+
LeafConditionalUpdateEntityFields,
|
|
739
|
+
'id',
|
|
740
|
+
ViewerContext,
|
|
741
|
+
LeafConditionalUpdateEntity
|
|
742
|
+
> {
|
|
743
|
+
protected override readonly readRules = [
|
|
744
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
745
|
+
LeafConditionalUpdateEntityFields,
|
|
746
|
+
'id',
|
|
747
|
+
ViewerContext,
|
|
748
|
+
LeafConditionalUpdateEntity
|
|
749
|
+
>(),
|
|
750
|
+
];
|
|
751
|
+
protected override readonly createRules = [
|
|
752
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
753
|
+
LeafConditionalUpdateEntityFields,
|
|
754
|
+
'id',
|
|
755
|
+
ViewerContext,
|
|
756
|
+
LeafConditionalUpdateEntity
|
|
757
|
+
>(),
|
|
758
|
+
];
|
|
759
|
+
protected override readonly updateRules = [
|
|
760
|
+
{
|
|
761
|
+
async evaluateAsync(
|
|
762
|
+
_viewerContext: ViewerContext,
|
|
763
|
+
_queryContext: EntityQueryContext,
|
|
764
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext<
|
|
765
|
+
LeafConditionalUpdateEntityFields,
|
|
766
|
+
'id',
|
|
767
|
+
ViewerContext,
|
|
768
|
+
LeafConditionalUpdateEntity
|
|
769
|
+
>,
|
|
770
|
+
entity: LeafConditionalUpdateEntity,
|
|
771
|
+
): Promise<RuleEvaluationResult> {
|
|
772
|
+
// Only allow updates when parent_id is being set to null
|
|
773
|
+
// and parent_id was previously non-null and was the cascading delete cause
|
|
774
|
+
|
|
775
|
+
const { previousValue, cascadingDeleteCause } = evaluationContext;
|
|
776
|
+
if (!previousValue || !cascadingDeleteCause) {
|
|
777
|
+
return RuleEvaluationResult.SKIP;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const parentId = entity.getField('parent_id');
|
|
781
|
+
const previousParentId = previousValue.getField('parent_id');
|
|
782
|
+
|
|
783
|
+
if (parentId !== null) {
|
|
784
|
+
return RuleEvaluationResult.SKIP;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!previousParentId) {
|
|
788
|
+
return RuleEvaluationResult.SKIP;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (cascadingDeleteCause.entity.getID() !== previousParentId) {
|
|
792
|
+
return RuleEvaluationResult.SKIP;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return RuleEvaluationResult.ALLOW;
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
];
|
|
799
|
+
protected override readonly deleteRules = [
|
|
800
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
801
|
+
LeafConditionalUpdateEntityFields,
|
|
802
|
+
'id',
|
|
803
|
+
ViewerContext,
|
|
804
|
+
LeafConditionalUpdateEntity
|
|
805
|
+
>(),
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Privacy policy that denies updates when parent_id is null
|
|
810
|
+
class DenyUpdateWhenNullEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
811
|
+
LeafConditionalUpdateEntityFields,
|
|
812
|
+
'id',
|
|
813
|
+
ViewerContext,
|
|
814
|
+
LeafConditionalUpdateEntity
|
|
815
|
+
> {
|
|
816
|
+
protected override readonly readRules = [
|
|
817
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
818
|
+
LeafConditionalUpdateEntityFields,
|
|
819
|
+
'id',
|
|
820
|
+
ViewerContext,
|
|
821
|
+
LeafConditionalUpdateEntity
|
|
822
|
+
>(),
|
|
823
|
+
];
|
|
824
|
+
protected override readonly createRules = [
|
|
825
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
826
|
+
LeafConditionalUpdateEntityFields,
|
|
827
|
+
'id',
|
|
828
|
+
ViewerContext,
|
|
829
|
+
LeafConditionalUpdateEntity
|
|
830
|
+
>(),
|
|
831
|
+
];
|
|
832
|
+
protected override readonly updateRules = [
|
|
833
|
+
{
|
|
834
|
+
async evaluateAsync(
|
|
835
|
+
_viewerContext: ViewerContext,
|
|
836
|
+
_queryContext: EntityQueryContext,
|
|
837
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext<
|
|
838
|
+
LeafConditionalUpdateEntityFields,
|
|
839
|
+
'id',
|
|
840
|
+
ViewerContext,
|
|
841
|
+
LeafConditionalUpdateEntity
|
|
842
|
+
>,
|
|
843
|
+
entity: LeafConditionalUpdateEntity,
|
|
844
|
+
): Promise<RuleEvaluationResult> {
|
|
845
|
+
// Deny updates when parent_id is being set to null
|
|
846
|
+
|
|
847
|
+
const { previousValue } = evaluationContext;
|
|
848
|
+
if (!previousValue) {
|
|
849
|
+
return RuleEvaluationResult.SKIP;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const parentId = entity.getField('parent_id');
|
|
853
|
+
const previousParentId = previousValue.getField('parent_id');
|
|
854
|
+
|
|
855
|
+
if (!parentId && previousParentId) {
|
|
856
|
+
return RuleEvaluationResult.DENY;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return RuleEvaluationResult.SKIP;
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
];
|
|
863
|
+
protected override readonly deleteRules = [
|
|
864
|
+
new AlwaysAllowPrivacyPolicyRule<
|
|
865
|
+
LeafConditionalUpdateEntityFields,
|
|
866
|
+
'id',
|
|
867
|
+
ViewerContext,
|
|
868
|
+
LeafConditionalUpdateEntity
|
|
869
|
+
>(),
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
class ParentEntity extends Entity<ParentEntityFields, 'id', ViewerContext> {
|
|
874
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
875
|
+
ParentEntityFields,
|
|
876
|
+
'id',
|
|
877
|
+
ViewerContext,
|
|
878
|
+
ParentEntity,
|
|
879
|
+
DenyUpdateEntityPrivacyPolicy<ParentEntityFields, 'id', ViewerContext, ParentEntity>
|
|
880
|
+
> {
|
|
881
|
+
return {
|
|
882
|
+
entityClass: ParentEntity,
|
|
883
|
+
entityConfiguration: new EntityConfiguration<ParentEntityFields, 'id'>({
|
|
884
|
+
idField: 'id',
|
|
885
|
+
tableName: 'parent_entity',
|
|
886
|
+
inboundEdges: [LeafConditionalUpdateEntity, LeafDenyUpdateWhenNullEntity],
|
|
887
|
+
schema: {
|
|
888
|
+
id: new UUIDField({
|
|
889
|
+
columnName: 'id',
|
|
890
|
+
cache: false,
|
|
891
|
+
}),
|
|
892
|
+
},
|
|
893
|
+
databaseAdapterFlavor: 'postgres',
|
|
894
|
+
cacheAdapterFlavor: 'redis',
|
|
895
|
+
}),
|
|
896
|
+
privacyPolicyClass: DenyUpdateEntityPrivacyPolicy,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
class LeafConditionalUpdateEntity extends Entity<
|
|
902
|
+
LeafConditionalUpdateEntityFields,
|
|
903
|
+
'id',
|
|
904
|
+
ViewerContext
|
|
905
|
+
> {
|
|
906
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
907
|
+
LeafConditionalUpdateEntityFields,
|
|
908
|
+
'id',
|
|
909
|
+
ViewerContext,
|
|
910
|
+
LeafConditionalUpdateEntity,
|
|
911
|
+
ConditionalUpdateEntityPrivacyPolicy
|
|
912
|
+
> {
|
|
913
|
+
return {
|
|
914
|
+
entityClass: LeafConditionalUpdateEntity,
|
|
915
|
+
entityConfiguration: new EntityConfiguration<LeafConditionalUpdateEntityFields, 'id'>({
|
|
916
|
+
idField: 'id',
|
|
917
|
+
tableName: 'leaf_conditional_update',
|
|
918
|
+
schema: {
|
|
919
|
+
id: new UUIDField({
|
|
920
|
+
columnName: 'id',
|
|
921
|
+
cache: false,
|
|
922
|
+
}),
|
|
923
|
+
parent_id: new UUIDField({
|
|
924
|
+
columnName: 'parent_id',
|
|
925
|
+
association: {
|
|
926
|
+
associatedEntityClass: ParentEntity,
|
|
927
|
+
edgeDeletionBehavior: EntityEdgeDeletionBehavior.SET_NULL,
|
|
928
|
+
},
|
|
929
|
+
}),
|
|
930
|
+
},
|
|
931
|
+
databaseAdapterFlavor: 'postgres',
|
|
932
|
+
cacheAdapterFlavor: 'redis',
|
|
933
|
+
}),
|
|
934
|
+
privacyPolicyClass: ConditionalUpdateEntityPrivacyPolicy,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
class LeafDenyUpdateWhenNullEntity extends Entity<
|
|
940
|
+
LeafDenyUpdateWhenNullEntityFields,
|
|
941
|
+
'id',
|
|
942
|
+
ViewerContext
|
|
943
|
+
> {
|
|
944
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
945
|
+
LeafDenyUpdateWhenNullEntityFields,
|
|
946
|
+
'id',
|
|
947
|
+
ViewerContext,
|
|
948
|
+
LeafDenyUpdateWhenNullEntity,
|
|
949
|
+
DenyUpdateWhenNullEntityPrivacyPolicy
|
|
950
|
+
> {
|
|
951
|
+
return {
|
|
952
|
+
entityClass: LeafDenyUpdateWhenNullEntity,
|
|
953
|
+
entityConfiguration: new EntityConfiguration<LeafDenyUpdateWhenNullEntityFields, 'id'>({
|
|
954
|
+
idField: 'id',
|
|
955
|
+
tableName: 'leaf_deny_update_when_null',
|
|
956
|
+
schema: {
|
|
957
|
+
id: new UUIDField({
|
|
958
|
+
columnName: 'id',
|
|
959
|
+
cache: false,
|
|
960
|
+
}),
|
|
961
|
+
parent_id: new UUIDField({
|
|
962
|
+
columnName: 'parent_id',
|
|
963
|
+
association: {
|
|
964
|
+
associatedEntityClass: ParentEntity,
|
|
965
|
+
edgeDeletionBehavior: EntityEdgeDeletionBehavior.SET_NULL,
|
|
966
|
+
},
|
|
967
|
+
}),
|
|
968
|
+
},
|
|
969
|
+
databaseAdapterFlavor: 'postgres',
|
|
970
|
+
cacheAdapterFlavor: 'redis',
|
|
971
|
+
}),
|
|
972
|
+
privacyPolicyClass: DenyUpdateWhenNullEntityPrivacyPolicy,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|