@expo/entity 0.38.0 → 0.39.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.
Files changed (51) hide show
  1. package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
  2. package/build/EntityCompanionProvider.d.ts +2 -2
  3. package/build/EntityCompanionProvider.js.map +1 -1
  4. package/build/EntityDatabaseAdapter.js +2 -2
  5. package/build/EntityDatabaseAdapter.js.map +1 -1
  6. package/build/EntityMutator.js +1 -1
  7. package/build/EntityMutator.js.map +1 -1
  8. package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
  9. package/build/__tests__/EntityCompanion-test.js.map +1 -1
  10. package/build/__tests__/EntityDatabaseAdapter-test.js.map +1 -1
  11. package/build/__tests__/EntityLoader-constructor-test.js +1 -1
  12. package/build/__tests__/entityUtils-test.js +8 -0
  13. package/build/__tests__/entityUtils-test.js.map +1 -1
  14. package/build/entityUtils.d.ts +7 -0
  15. package/build/entityUtils.js +20 -10
  16. package/build/entityUtils.js.map +1 -1
  17. package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
  18. package/build/internal/__tests__/EntityDataManager-test.js.map +1 -1
  19. package/build/utils/EntityPrivacyUtils.d.ts +32 -4
  20. package/build/utils/EntityPrivacyUtils.js +64 -16
  21. package/build/utils/EntityPrivacyUtils.js.map +1 -1
  22. package/build/utils/__tests__/EntityPrivacyUtils-test.js +85 -4
  23. package/build/utils/__tests__/EntityPrivacyUtils-test.js.map +1 -1
  24. package/build/utils/collections/__tests__/maps-test.js +1 -1
  25. package/build/utils/collections/__tests__/maps-test.js.map +1 -1
  26. package/build/utils/collections/maps.js +2 -2
  27. package/build/utils/collections/maps.js.map +1 -1
  28. package/build/utils/mergeEntityMutationTriggerConfigurations.js +1 -2
  29. package/build/utils/mergeEntityMutationTriggerConfigurations.js.map +1 -1
  30. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +1 -1
  31. package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
  32. package/build/utils/testing/StubDatabaseAdapter.js.map +1 -1
  33. package/build/utils/testing/describeFieldTestCase.js +1 -1
  34. package/build/utils/testing/describeFieldTestCase.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/AuthorizationResultBasedEntityLoader.ts +1 -1
  37. package/src/EntityCompanionProvider.ts +5 -2
  38. package/src/EntityMutator.ts +1 -1
  39. package/src/__tests__/EntityCommonUseCases-test.ts +4 -4
  40. package/src/__tests__/EntityCompanion-test.ts +1 -1
  41. package/src/__tests__/EntityDatabaseAdapter-test.ts +6 -6
  42. package/src/__tests__/EntityLoader-constructor-test.ts +1 -1
  43. package/src/__tests__/entityUtils-test.ts +12 -0
  44. package/src/entityUtils.ts +24 -9
  45. package/src/internal/EntityFieldTransformationUtils.ts +2 -2
  46. package/src/internal/__tests__/EntityDataManager-test.ts +4 -4
  47. package/src/utils/EntityPrivacyUtils.ts +178 -94
  48. package/src/utils/__tests__/EntityPrivacyUtils-test.ts +119 -5
  49. package/src/utils/collections/__tests__/maps-test.ts +1 -1
  50. package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +1 -1
  51. package/src/utils/testing/StubDatabaseAdapter.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"describeFieldTestCase.js","sourceRoot":"","sources":["../../../src/utils/testing/describeFieldTestCase.ts"],"names":[],"mappings":";;AAEA,SAAwB,qBAAqB,CAC3C,eAAyC,EACzC,WAAgB,EAChB,aAAoB;IAEpB,QAAQ,CAAC,eAAe,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE;QAC9C,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC/E,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/D,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;gBACnF,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAlBD,wCAkBC"}
1
+ {"version":3,"file":"describeFieldTestCase.js","sourceRoot":"","sources":["../../../src/utils/testing/describeFieldTestCase.ts"],"names":[],"mappings":";;AAEA,wCAkBC;AAlBD,SAAwB,qBAAqB,CAC3C,eAAyC,EACzC,WAAgB,EAChB,aAAoB;IAEpB,QAAQ,CAAC,eAAe,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE;QAC9C,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC/E,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/D,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;gBACnF,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/entity",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "A privacy-first data model",
5
5
  "files": [
6
6
  "build",
@@ -34,5 +34,5 @@
34
34
  "uuid": "^8.3.0",
35
35
  "uuidv7": "^1.0.0"
36
36
  },
37
- "gitHead": "d7cc0c23b983eccca9ca4e7ca620a5aaef6846c4"
37
+ "gitHead": "9426a08a3226d907bf43378b9ea57fd04284c4ea"
38
38
  }
@@ -102,7 +102,7 @@ export default class AuthorizationResultBasedEntityLoader<
102
102
  entityResultsForFieldValue !== undefined,
103
103
  `${fieldValue} should be guaranteed to be present in returned map of entities`,
104
104
  );
105
- return entityResultsForFieldValue!;
105
+ return entityResultsForFieldValue;
106
106
  }
107
107
 
108
108
  /**
@@ -143,11 +143,14 @@ export default class EntityCompanionProvider {
143
143
  */
144
144
  constructor(
145
145
  public readonly metricsAdapter: IEntityMetricsAdapter,
146
- private databaseAdapterFlavors: ReadonlyMap<
146
+ private readonly databaseAdapterFlavors: ReadonlyMap<
147
147
  DatabaseAdapterFlavor,
148
148
  DatabaseAdapterFlavorDefinition
149
149
  >,
150
- private cacheAdapterFlavors: ReadonlyMap<CacheAdapterFlavor, CacheAdapterFlavorDefinition>,
150
+ private readonly cacheAdapterFlavors: ReadonlyMap<
151
+ CacheAdapterFlavor,
152
+ CacheAdapterFlavorDefinition
153
+ >,
151
154
  readonly globalMutationTriggers: EntityMutationTriggerConfiguration<
152
155
  any,
153
156
  any,
@@ -611,7 +611,7 @@ export class DeleteMutator<
611
611
  * Convenience method that throws upon delete failure.
612
612
  */
613
613
  async enforceDeleteAsync(): Promise<void> {
614
- return await enforceAsyncResult(this.deleteAsync());
614
+ await enforceAsyncResult(this.deleteAsync());
615
615
  }
616
616
 
617
617
  private async deleteInTransactionAsync(
@@ -118,15 +118,15 @@ it('runs through a common workflow', async () => {
118
118
  const vc2 = new TestUserViewerContext(entityCompanionProvider, uuidv4());
119
119
 
120
120
  const blahOwner1 = await enforceAsyncResult(
121
- BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()!).createAsync(),
121
+ BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()).createAsync(),
122
122
  );
123
123
 
124
124
  await enforceAsyncResult(
125
- BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()!).createAsync(),
125
+ BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()).createAsync(),
126
126
  );
127
127
 
128
128
  const blahOwner2 = await enforceAsyncResult(
129
- BlahEntity.creator(vc2).setField('ownerID', vc2.getUserID()!).createAsync(),
129
+ BlahEntity.creator(vc2).setField('ownerID', vc2.getUserID()).createAsync(),
130
130
  );
131
131
 
132
132
  // sanity check created objects
@@ -149,7 +149,7 @@ it('runs through a common workflow', async () => {
149
149
  const results = await enforceResultsAsync(
150
150
  BlahEntity.loader(vc1)
151
151
  .withAuthorizationResults()
152
- .loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!),
152
+ .loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()),
153
153
  );
154
154
  expect(results).toHaveLength(2);
155
155
 
@@ -72,7 +72,7 @@ describe(EntityCompanion, () => {
72
72
 
73
73
  expect(mergedTriggers).toStrictEqual({
74
74
  afterCreate: [localTriggers!.afterCreate![0], globalMutationTriggers.afterCreate![0]],
75
- afterAll: [localTriggers!.afterAll![0], globalMutationTriggers!.afterAll![0]],
75
+ afterAll: [localTriggers!.afterAll![0], globalMutationTriggers.afterAll![0]],
76
76
  afterCommit: [localTriggers!.afterCommit![0]],
77
77
  });
78
78
  });
@@ -9,12 +9,12 @@ import { FieldTransformerMap } from '../internal/EntityFieldTransformationUtils'
9
9
  import { TestFields, testEntityConfiguration } from '../testfixtures/TestEntity';
10
10
 
11
11
  class TestEntityDatabaseAdapter extends EntityDatabaseAdapter<TestFields> {
12
- private fetchResults: object[];
13
- private insertResults: object[];
14
- private updateResults: object[];
15
- private fetchEqualityConditionResults: object[];
16
- private fetchRawWhereResults: object[];
17
- private deleteCount: number;
12
+ private readonly fetchResults: object[];
13
+ private readonly insertResults: object[];
14
+ private readonly updateResults: object[];
15
+ private readonly fetchEqualityConditionResults: object[];
16
+ private readonly fetchRawWhereResults: object[];
17
+ private readonly deleteCount: number;
18
18
 
19
19
  constructor({
20
20
  fetchResults = [],
@@ -94,7 +94,7 @@ export default class TestEntity extends Entity<
94
94
  selectedFields: Readonly<TestFields>;
95
95
  }) {
96
96
  if (constructorParams.selectedFields.id === ID_SENTINEL_THROW_LITERAL) {
97
- // eslint-disable-next-line no-throw-literal,@typescript-eslint/no-throw-literal
97
+ // eslint-disable-next-line no-throw-literal,@typescript-eslint/only-throw-error
98
98
  throw 'hello';
99
99
  } else if (constructorParams.selectedFields.id === ID_SENTINEL_THROW_ERROR) {
100
100
  throw new Error('world');
@@ -7,6 +7,7 @@ import {
7
7
  successfulResultsFilterMap,
8
8
  failedResultsFilterMap,
9
9
  pick,
10
+ partitionArray,
10
11
  } from '../entityUtils';
11
12
 
12
13
  describe(enforceResultsAsync, () => {
@@ -97,3 +98,14 @@ describe(pick, () => {
97
98
  });
98
99
  });
99
100
  });
101
+
102
+ describe(partitionArray, () => {
103
+ it('partitions array', () => {
104
+ type A = true;
105
+ type B = false;
106
+ const arr: (A | B)[] = [true, false, true, true, false];
107
+ const [as, bs] = partitionArray<A, B>(arr, (val: A | B): val is A => val === true);
108
+ expect(as).toMatchObject([true, true, true]);
109
+ expect(bs).toMatchObject([false, false]);
110
+ });
111
+ });
@@ -71,22 +71,37 @@ export const failedResultsFilterMap = <K, T>(
71
71
  return ret;
72
72
  };
73
73
 
74
+ export type PartitionArrayPredicate<T, U> = (val: T | U) => val is T;
75
+
74
76
  /**
75
- * Partition array of values and errors into an array of values and an array of errors.
76
- * @param valuesAndErrors - array of values and errors
77
+ * Partition an array of values into two arrays based on evaluation of a binary predicate.
78
+ * @param values - array of values to partition
79
+ * @param predicate - binary predicate to evaluate partition group of each value
77
80
  */
78
- export const partitionErrors = <T>(valuesAndErrors: (T | Error)[]): [T[], Error[]] => {
79
- const values: T[] = [];
80
- const errors: Error[] = [];
81
+ export const partitionArray = <T, U>(
82
+ values: (T | U)[],
83
+ predicate: PartitionArrayPredicate<T, U>,
84
+ ): [T[], U[]] => {
85
+ const ts: T[] = [];
86
+ const us: U[] = [];
81
87
 
82
- for (const valueOrError of valuesAndErrors) {
83
- if (isError(valueOrError)) {
84
- errors.push(valueOrError);
88
+ for (const value of values) {
89
+ if (predicate(value)) {
90
+ ts.push(value);
85
91
  } else {
86
- values.push(valueOrError);
92
+ us.push(value);
87
93
  }
88
94
  }
89
95
 
96
+ return [ts, us];
97
+ };
98
+
99
+ /**
100
+ * Partition array of values and errors into an array of values and an array of errors.
101
+ * @param valuesAndErrors - array of values and errors
102
+ */
103
+ export const partitionErrors = <T>(valuesAndErrors: (T | Error)[]): [T[], Error[]] => {
104
+ const [errors, values] = partitionArray<Error, T>(valuesAndErrors, isError);
90
105
  return [values, errors];
91
106
  };
92
107
 
@@ -26,7 +26,7 @@ export const getDatabaseFieldForEntityField = <TFields extends Record<string, an
26
26
  ): string => {
27
27
  const databaseField = entityConfiguration.entityToDBFieldsKeyMapping.get(entityField);
28
28
  invariant(databaseField, `database field mapping missing for ${String(entityField)}`);
29
- return databaseField!;
29
+ return databaseField;
30
30
  };
31
31
 
32
32
  export const transformDatabaseObjectToFields = <TFields extends Record<string, any>>(
@@ -60,7 +60,7 @@ export const transformFieldsToDatabaseObject = <TFields extends Record<string, a
60
60
  const val = fields[k]!;
61
61
  const databaseKey = entityConfiguration.entityToDBFieldsKeyMapping.get(k as any);
62
62
  invariant(databaseKey, `must be database key for field: ${k}`);
63
- databaseObject[databaseKey!] = maybeTransformFieldValueToDatabaseValue(
63
+ databaseObject[databaseKey] = maybeTransformFieldValueToDatabaseValue(
64
64
  entityConfiguration,
65
65
  fieldTransformerMap,
66
66
  k,
@@ -306,11 +306,11 @@ describe(EntityDataManager, () => {
306
306
  const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
307
307
 
308
308
  await entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'testIndexedField', [
309
- objectInQuestion['testIndexedField']!,
309
+ objectInQuestion['testIndexedField'],
310
310
  ]);
311
311
  await entityDataManager.invalidateObjectFieldsAsync(objectInQuestion);
312
312
  await entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'testIndexedField', [
313
- objectInQuestion['testIndexedField']!,
313
+ objectInQuestion['testIndexedField'],
314
314
  ]);
315
315
 
316
316
  expect(dbSpy).toHaveBeenCalledTimes(2);
@@ -345,11 +345,11 @@ describe(EntityDataManager, () => {
345
345
  const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
346
346
 
347
347
  await entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'testIndexedField', [
348
- objectInQuestion['testIndexedField']!,
348
+ objectInQuestion['testIndexedField'],
349
349
  ]);
350
350
  await entityDataManager.invalidateObjectFieldsAsync(objectInQuestion);
351
351
  await entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'customIdField', [
352
- objectInQuestion['customIdField']!,
352
+ objectInQuestion['customIdField'],
353
353
  ]);
354
354
 
355
355
  expect(dbSpy).toHaveBeenCalledTimes(2);
@@ -9,9 +9,22 @@ import { EntityCascadingDeletionInfo } from '../EntityMutationInfo';
9
9
  import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
10
10
  import { EntityQueryContext } from '../EntityQueryContext';
11
11
  import ViewerContext from '../ViewerContext';
12
- import { failedResults } from '../entityUtils';
12
+ import { failedResults, partitionArray } from '../entityUtils';
13
13
  import EntityNotAuthorizedError from '../errors/EntityNotAuthorizedError';
14
14
 
15
+ export type EntityPrivacyEvaluationResultSuccess = {
16
+ allowed: true;
17
+ };
18
+
19
+ export type EntityPrivacyEvaluationResultFailure = {
20
+ allowed: false;
21
+ authorizationErrors: EntityNotAuthorizedError<any, any, any, any, any>[];
22
+ };
23
+
24
+ export type EntityPrivacyEvaluationResult =
25
+ | EntityPrivacyEvaluationResultSuccess
26
+ | EntityPrivacyEvaluationResultFailure;
27
+
15
28
  /**
16
29
  * Check whether an entity loaded by a viewer can be updated by that same viewer.
17
30
  *
@@ -30,34 +43,67 @@ import EntityNotAuthorizedError from '../errors/EntityNotAuthorizedError';
30
43
  * @param queryContext - query context in which to perform the check
31
44
  */
32
45
  export async function canViewerUpdateAsync<
33
- TMFields extends object,
34
- TMID extends NonNullable<TMFields[TMSelectedFields]>,
35
- TMViewerContext extends ViewerContext,
36
- TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
37
- TMPrivacyPolicy extends EntityPrivacyPolicy<
38
- TMFields,
39
- TMID,
40
- TMViewerContext,
41
- TMEntity,
42
- TMSelectedFields
46
+ TFields extends object,
47
+ TID extends NonNullable<TFields[TSelectedFields]>,
48
+ TViewerContext extends ViewerContext,
49
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
50
+ TPrivacyPolicy extends EntityPrivacyPolicy<
51
+ TFields,
52
+ TID,
53
+ TViewerContext,
54
+ TEntity,
55
+ TSelectedFields
43
56
  >,
44
- TMSelectedFields extends keyof TMFields = keyof TMFields,
57
+ TSelectedFields extends keyof TFields = keyof TFields,
45
58
  >(
46
- entityClass: IEntityClass<
47
- TMFields,
48
- TMID,
49
- TMViewerContext,
50
- TMEntity,
51
- TMPrivacyPolicy,
52
- TMSelectedFields
53
- >,
54
- sourceEntity: TMEntity,
59
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
60
+ sourceEntity: TEntity,
55
61
  queryContext: EntityQueryContext = sourceEntity
56
62
  .getViewerContext()
57
63
  .getViewerScopedEntityCompanionForClass(entityClass)
58
64
  .getQueryContextProvider()
59
65
  .getQueryContext(),
60
66
  ): Promise<boolean> {
67
+ const result = await canViewerUpdateInternalAsync(
68
+ entityClass,
69
+ sourceEntity,
70
+ /* cascadingDeleteCause */ null,
71
+ queryContext,
72
+ );
73
+ return result.allowed;
74
+ }
75
+
76
+ /**
77
+ * Check whether an entity loaded by a viewer can be updated by that same viewer and return the evaluation result.
78
+ *
79
+ * @see canViewerUpdateAsync
80
+ *
81
+ * @param entityClass - class of entity
82
+ * @param sourceEntity - entity loaded by viewer
83
+ * @param queryContext - query context in which to perform the check
84
+ */
85
+ export async function getCanViewerUpdateResultAsync<
86
+ TFields extends object,
87
+ TID extends NonNullable<TFields[TSelectedFields]>,
88
+ TViewerContext extends ViewerContext,
89
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
90
+ TPrivacyPolicy extends EntityPrivacyPolicy<
91
+ TFields,
92
+ TID,
93
+ TViewerContext,
94
+ TEntity,
95
+ TSelectedFields
96
+ >,
97
+ TSelectedFields extends keyof TFields = keyof TFields,
98
+ >(
99
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
100
+ sourceEntity: TEntity,
101
+ queryContext: EntityQueryContext = sourceEntity
102
+ .getViewerContext()
103
+ .getViewerScopedEntityCompanionForClass(entityClass)
104
+ .getQueryContextProvider()
105
+ .getQueryContext(),
106
+ ): Promise<EntityPrivacyEvaluationResult> {
61
107
  return await canViewerUpdateInternalAsync(
62
108
  entityClass,
63
109
  sourceEntity,
@@ -67,31 +113,24 @@ export async function canViewerUpdateAsync<
67
113
  }
68
114
 
69
115
  async function canViewerUpdateInternalAsync<
70
- TMFields extends object,
71
- TMID extends NonNullable<TMFields[TMSelectedFields]>,
72
- TMViewerContext extends ViewerContext,
73
- TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
74
- TMPrivacyPolicy extends EntityPrivacyPolicy<
75
- TMFields,
76
- TMID,
77
- TMViewerContext,
78
- TMEntity,
79
- TMSelectedFields
116
+ TFields extends object,
117
+ TID extends NonNullable<TFields[TSelectedFields]>,
118
+ TViewerContext extends ViewerContext,
119
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
120
+ TPrivacyPolicy extends EntityPrivacyPolicy<
121
+ TFields,
122
+ TID,
123
+ TViewerContext,
124
+ TEntity,
125
+ TSelectedFields
80
126
  >,
81
- TMSelectedFields extends keyof TMFields = keyof TMFields,
127
+ TSelectedFields extends keyof TFields = keyof TFields,
82
128
  >(
83
- entityClass: IEntityClass<
84
- TMFields,
85
- TMID,
86
- TMViewerContext,
87
- TMEntity,
88
- TMPrivacyPolicy,
89
- TMSelectedFields
90
- >,
91
- sourceEntity: TMEntity,
129
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
130
+ sourceEntity: TEntity,
92
131
  cascadingDeleteCause: EntityCascadingDeletionInfo | null,
93
132
  queryContext: EntityQueryContext,
94
- ): Promise<boolean> {
133
+ ): Promise<EntityPrivacyEvaluationResult> {
95
134
  const companion = sourceEntity
96
135
  .getViewerContext()
97
136
  .getViewerScopedEntityCompanionForClass(entityClass);
@@ -107,20 +146,19 @@ async function canViewerUpdateInternalAsync<
107
146
  );
108
147
  if (!evaluationResult.ok) {
109
148
  if (evaluationResult.reason instanceof EntityNotAuthorizedError) {
110
- return false;
149
+ return { allowed: false, authorizationErrors: [evaluationResult.reason] };
111
150
  } else {
112
151
  throw evaluationResult.reason;
113
152
  }
114
153
  }
115
- return evaluationResult.ok;
154
+ return { allowed: true };
116
155
  }
117
156
 
118
157
  /**
119
158
  * Check whether a single entity loaded by a viewer can be deleted by that same viewer.
120
159
  * This recursively checks edge cascade permissions (EntityEdgeDeletionBehavior) as well.
121
160
  *
122
- * @remarks
123
- * See remarks for canViewerUpdate.
161
+ * @see canViewerUpdateAsync
124
162
  *
125
163
  * @param entityClass - class of entity
126
164
  * @param sourceEntity - entity loaded by viewer
@@ -129,32 +167,65 @@ async function canViewerUpdateInternalAsync<
129
167
  export async function canViewerDeleteAsync<
130
168
  TFields extends object,
131
169
  TID extends NonNullable<TFields[TSelectedFields]>,
132
- TMViewerContext extends ViewerContext,
133
- TEntity extends Entity<TFields, TID, TMViewerContext, TSelectedFields>,
170
+ TViewerContext extends ViewerContext,
171
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
134
172
  TPrivacyPolicy extends EntityPrivacyPolicy<
135
173
  TFields,
136
174
  TID,
137
- TMViewerContext,
175
+ TViewerContext,
138
176
  TEntity,
139
177
  TSelectedFields
140
178
  >,
141
179
  TSelectedFields extends keyof TFields = keyof TFields,
142
180
  >(
143
- entityClass: IEntityClass<
181
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
182
+ sourceEntity: TEntity,
183
+ queryContext: EntityQueryContext = sourceEntity
184
+ .getViewerContext()
185
+ .getViewerScopedEntityCompanionForClass(entityClass)
186
+ .getQueryContextProvider()
187
+ .getQueryContext(),
188
+ ): Promise<boolean> {
189
+ const result = await canViewerDeleteInternalAsync(
190
+ entityClass,
191
+ sourceEntity,
192
+ /* cascadingDeleteCause */ null,
193
+ queryContext,
194
+ );
195
+ return result.allowed;
196
+ }
197
+
198
+ /**
199
+ * Check whether a single entity loaded by a viewer can be deleted by that same viewer and return the evaluation result.
200
+ *
201
+ * @see canViewerDeleteAsync
202
+ *
203
+ * @param entityClass - class of entity
204
+ * @param sourceEntity - entity loaded by viewer
205
+ * @param queryContext - query context in which to perform the check
206
+ */
207
+ export async function getCanViewerDeleteResultAsync<
208
+ TFields extends object,
209
+ TID extends NonNullable<TFields[TSelectedFields]>,
210
+ TViewerContext extends ViewerContext,
211
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
212
+ TPrivacyPolicy extends EntityPrivacyPolicy<
144
213
  TFields,
145
214
  TID,
146
- TMViewerContext,
215
+ TViewerContext,
147
216
  TEntity,
148
- TPrivacyPolicy,
149
217
  TSelectedFields
150
218
  >,
219
+ TSelectedFields extends keyof TFields = keyof TFields,
220
+ >(
221
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
151
222
  sourceEntity: TEntity,
152
223
  queryContext: EntityQueryContext = sourceEntity
153
224
  .getViewerContext()
154
225
  .getViewerScopedEntityCompanionForClass(entityClass)
155
226
  .getQueryContextProvider()
156
227
  .getQueryContext(),
157
- ): Promise<boolean> {
228
+ ): Promise<EntityPrivacyEvaluationResult> {
158
229
  return await canViewerDeleteInternalAsync(
159
230
  entityClass,
160
231
  sourceEntity,
@@ -166,29 +237,22 @@ export async function canViewerDeleteAsync<
166
237
  async function canViewerDeleteInternalAsync<
167
238
  TFields extends object,
168
239
  TID extends NonNullable<TFields[TSelectedFields]>,
169
- TMViewerContext extends ViewerContext,
170
- TEntity extends Entity<TFields, TID, TMViewerContext, TSelectedFields>,
240
+ TViewerContext extends ViewerContext,
241
+ TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
171
242
  TPrivacyPolicy extends EntityPrivacyPolicy<
172
243
  TFields,
173
244
  TID,
174
- TMViewerContext,
245
+ TViewerContext,
175
246
  TEntity,
176
247
  TSelectedFields
177
248
  >,
178
249
  TSelectedFields extends keyof TFields = keyof TFields,
179
250
  >(
180
- entityClass: IEntityClass<
181
- TFields,
182
- TID,
183
- TMViewerContext,
184
- TEntity,
185
- TPrivacyPolicy,
186
- TSelectedFields
187
- >,
251
+ entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>,
188
252
  sourceEntity: TEntity,
189
253
  cascadingDeleteCause: EntityCascadingDeletionInfo | null,
190
254
  queryContext: EntityQueryContext,
191
- ): Promise<boolean> {
255
+ ): Promise<EntityPrivacyEvaluationResult> {
192
256
  const viewerContext = sourceEntity.getViewerContext();
193
257
  const entityCompanionProvider = viewerContext.entityCompanionProvider;
194
258
  const viewerScopedCompanion = sourceEntity
@@ -207,7 +271,7 @@ async function canViewerDeleteInternalAsync<
207
271
  );
208
272
  if (!evaluationResult.ok) {
209
273
  if (evaluationResult.reason instanceof EntityNotAuthorizedError) {
210
- return false;
274
+ return { allowed: false, authorizationErrors: [evaluationResult.reason] };
211
275
  } else {
212
276
  throw evaluationResult.reason;
213
277
  }
@@ -297,7 +361,7 @@ async function canViewerDeleteInternalAsync<
297
361
  const failedEntityLoadResults = failedResults(entityResultsToCheckForInboundEdge);
298
362
  for (const failedResult of failedEntityLoadResults) {
299
363
  if (failedResult.reason instanceof EntityNotAuthorizedError) {
300
- return false;
364
+ return { allowed: false, authorizationErrors: [failedResult.reason] };
301
365
  } else {
302
366
  throw failedResult.reason;
303
367
  }
@@ -311,48 +375,68 @@ async function canViewerDeleteInternalAsync<
311
375
  switch (association.edgeDeletionBehavior) {
312
376
  case EntityEdgeDeletionBehavior.CASCADE_DELETE:
313
377
  case EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE_ONLY: {
314
- const canDeleteAll = (
315
- await Promise.all(
316
- entitiesForInboundEdge.map((entity) =>
317
- canViewerDeleteInternalAsync(
318
- inboundEdge,
319
- entity,
320
- newCascadingDeleteCause,
321
- queryContext,
322
- ),
378
+ const canDeleteEvaluationResults = await Promise.all(
379
+ entitiesForInboundEdge.map((entity) =>
380
+ canViewerDeleteInternalAsync(
381
+ inboundEdge,
382
+ entity,
383
+ newCascadingDeleteCause,
384
+ queryContext,
323
385
  ),
324
- )
325
- ).every((b) => b);
386
+ ),
387
+ );
326
388
 
327
- if (!canDeleteAll) {
328
- return false;
389
+ const reducedEvaluationResult = reduceEvaluationResults(canDeleteEvaluationResults);
390
+ if (!reducedEvaluationResult.allowed) {
391
+ return reducedEvaluationResult;
329
392
  }
393
+
330
394
  break;
331
395
  }
332
396
 
333
397
  case EntityEdgeDeletionBehavior.SET_NULL:
334
398
  case EntityEdgeDeletionBehavior.SET_NULL_INVALIDATE_CACHE_ONLY: {
335
- const canUpdateAll = (
336
- await Promise.all(
337
- entitiesForInboundEdge.map((entity) =>
338
- canViewerUpdateInternalAsync(
339
- inboundEdge,
340
- entity,
341
- newCascadingDeleteCause,
342
- queryContext,
343
- ),
399
+ const canUpdateEvaluationResults = await Promise.all(
400
+ entitiesForInboundEdge.map((entity) =>
401
+ canViewerUpdateInternalAsync(
402
+ inboundEdge,
403
+ entity,
404
+ newCascadingDeleteCause,
405
+ queryContext,
344
406
  ),
345
- )
346
- ).every((b) => b);
407
+ ),
408
+ );
347
409
 
348
- if (!canUpdateAll) {
349
- return false;
410
+ const reducedEvaluationResult = reduceEvaluationResults(canUpdateEvaluationResults);
411
+ if (!reducedEvaluationResult.allowed) {
412
+ return reducedEvaluationResult;
350
413
  }
414
+
351
415
  break;
352
416
  }
353
417
  }
354
418
  }
355
419
  }
356
420
 
357
- return true;
421
+ return { allowed: true };
422
+ }
423
+
424
+ function reduceEvaluationResults(
425
+ evaluationResults: EntityPrivacyEvaluationResult[],
426
+ ): EntityPrivacyEvaluationResult {
427
+ const [successResults, failureResults] = partitionArray<
428
+ EntityPrivacyEvaluationResultSuccess,
429
+ EntityPrivacyEvaluationResultFailure
430
+ >(evaluationResults, (evaluationResult) => evaluationResult.allowed);
431
+
432
+ if (successResults.length === evaluationResults.length) {
433
+ return { allowed: true };
434
+ }
435
+
436
+ return {
437
+ allowed: false,
438
+ authorizationErrors: failureResults.flatMap(
439
+ (failureResult) => failureResult.authorizationErrors,
440
+ ),
441
+ };
358
442
  }