@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.
- package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
- package/build/EntityCompanionProvider.d.ts +2 -2
- package/build/EntityCompanionProvider.js.map +1 -1
- package/build/EntityDatabaseAdapter.js +2 -2
- package/build/EntityDatabaseAdapter.js.map +1 -1
- package/build/EntityMutator.js +1 -1
- package/build/EntityMutator.js.map +1 -1
- package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
- package/build/__tests__/EntityCompanion-test.js.map +1 -1
- package/build/__tests__/EntityDatabaseAdapter-test.js.map +1 -1
- package/build/__tests__/EntityLoader-constructor-test.js +1 -1
- package/build/__tests__/entityUtils-test.js +8 -0
- package/build/__tests__/entityUtils-test.js.map +1 -1
- package/build/entityUtils.d.ts +7 -0
- package/build/entityUtils.js +20 -10
- package/build/entityUtils.js.map +1 -1
- package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
- package/build/internal/__tests__/EntityDataManager-test.js.map +1 -1
- package/build/utils/EntityPrivacyUtils.d.ts +32 -4
- package/build/utils/EntityPrivacyUtils.js +64 -16
- package/build/utils/EntityPrivacyUtils.js.map +1 -1
- package/build/utils/__tests__/EntityPrivacyUtils-test.js +85 -4
- package/build/utils/__tests__/EntityPrivacyUtils-test.js.map +1 -1
- package/build/utils/collections/__tests__/maps-test.js +1 -1
- package/build/utils/collections/__tests__/maps-test.js.map +1 -1
- package/build/utils/collections/maps.js +2 -2
- package/build/utils/collections/maps.js.map +1 -1
- package/build/utils/mergeEntityMutationTriggerConfigurations.js +1 -2
- package/build/utils/mergeEntityMutationTriggerConfigurations.js.map +1 -1
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +1 -1
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
- package/build/utils/testing/StubDatabaseAdapter.js.map +1 -1
- package/build/utils/testing/describeFieldTestCase.js +1 -1
- package/build/utils/testing/describeFieldTestCase.js.map +1 -1
- package/package.json +2 -2
- package/src/AuthorizationResultBasedEntityLoader.ts +1 -1
- package/src/EntityCompanionProvider.ts +5 -2
- package/src/EntityMutator.ts +1 -1
- package/src/__tests__/EntityCommonUseCases-test.ts +4 -4
- package/src/__tests__/EntityCompanion-test.ts +1 -1
- package/src/__tests__/EntityDatabaseAdapter-test.ts +6 -6
- package/src/__tests__/EntityLoader-constructor-test.ts +1 -1
- package/src/__tests__/entityUtils-test.ts +12 -0
- package/src/entityUtils.ts +24 -9
- package/src/internal/EntityFieldTransformationUtils.ts +2 -2
- package/src/internal/__tests__/EntityDataManager-test.ts +4 -4
- package/src/utils/EntityPrivacyUtils.ts +178 -94
- package/src/utils/__tests__/EntityPrivacyUtils-test.ts +119 -5
- package/src/utils/collections/__tests__/maps-test.ts +1 -1
- package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +1 -1
- 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
|
|
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.
|
|
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": "
|
|
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<
|
|
150
|
+
private readonly cacheAdapterFlavors: ReadonlyMap<
|
|
151
|
+
CacheAdapterFlavor,
|
|
152
|
+
CacheAdapterFlavorDefinition
|
|
153
|
+
>,
|
|
151
154
|
readonly globalMutationTriggers: EntityMutationTriggerConfiguration<
|
|
152
155
|
any,
|
|
153
156
|
any,
|
package/src/EntityMutator.ts
CHANGED
|
@@ -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
|
-
|
|
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()
|
|
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()
|
|
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()
|
|
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
|
|
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/
|
|
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
|
+
});
|
package/src/entityUtils.ts
CHANGED
|
@@ -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
|
|
76
|
-
* @param
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
|
|
88
|
+
for (const value of values) {
|
|
89
|
+
if (predicate(value)) {
|
|
90
|
+
ts.push(value);
|
|
85
91
|
} else {
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
57
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
45
58
|
>(
|
|
46
|
-
entityClass: IEntityClass<
|
|
47
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
127
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
82
128
|
>(
|
|
83
|
-
entityClass: IEntityClass<
|
|
84
|
-
|
|
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<
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
|
|
133
|
-
TEntity extends Entity<TFields, TID,
|
|
170
|
+
TViewerContext extends ViewerContext,
|
|
171
|
+
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
|
134
172
|
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
135
173
|
TFields,
|
|
136
174
|
TID,
|
|
137
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
170
|
-
TEntity extends Entity<TFields, TID,
|
|
240
|
+
TViewerContext extends ViewerContext,
|
|
241
|
+
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
|
171
242
|
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
172
243
|
TFields,
|
|
173
244
|
TID,
|
|
174
|
-
|
|
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<
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
)
|
|
386
|
+
),
|
|
387
|
+
);
|
|
326
388
|
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
)
|
|
407
|
+
),
|
|
408
|
+
);
|
|
347
409
|
|
|
348
|
-
|
|
349
|
-
|
|
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
|
}
|