@expo/entity 0.43.0 → 0.45.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 +5 -1
- package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
- package/build/AuthorizationResultBasedEntityMutator.d.ts +87 -2
- package/build/AuthorizationResultBasedEntityMutator.js +122 -8
- package/build/AuthorizationResultBasedEntityMutator.js.map +1 -1
- package/build/EntityLoaderUtils.d.ts +15 -3
- package/build/EntityLoaderUtils.js +25 -10
- package/build/EntityLoaderUtils.js.map +1 -1
- package/build/EntityQueryContext.d.ts +53 -7
- package/build/EntityQueryContext.js +65 -10
- package/build/EntityQueryContext.js.map +1 -1
- package/build/EntityQueryContextProvider.d.ts +5 -1
- package/build/EntityQueryContextProvider.js +11 -4
- package/build/EntityQueryContextProvider.js.map +1 -1
- package/build/IEntityGenericCacher.d.ts +2 -2
- package/build/errors/EntityNotFoundError.d.ts +8 -1
- package/build/errors/EntityNotFoundError.js +7 -2
- package/build/errors/EntityNotFoundError.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/internal/CompositeFieldHolder.d.ts +13 -0
- package/build/internal/CompositeFieldHolder.js +7 -0
- package/build/internal/CompositeFieldHolder.js.map +1 -1
- package/build/internal/CompositeFieldValueMap.d.ts +3 -0
- package/build/internal/CompositeFieldValueMap.js +3 -0
- package/build/internal/CompositeFieldValueMap.js.map +1 -1
- package/build/internal/EntityDataManager.d.ts +22 -3
- package/build/internal/EntityDataManager.js +99 -11
- package/build/internal/EntityDataManager.js.map +1 -1
- package/build/internal/EntityFieldTransformationUtils.d.ts +20 -0
- package/build/internal/EntityFieldTransformationUtils.js +15 -0
- package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
- package/build/internal/EntityLoadInterfaces.d.ts +8 -0
- package/build/internal/EntityLoadInterfaces.js +2 -0
- package/build/internal/EntityLoadInterfaces.js.map +1 -1
- package/build/internal/EntityTableDataCoordinator.d.ts +2 -0
- package/build/internal/EntityTableDataCoordinator.js +2 -0
- package/build/internal/EntityTableDataCoordinator.js.map +1 -1
- package/build/internal/ReadThroughEntityCache.d.ts +8 -0
- package/build/internal/ReadThroughEntityCache.js +5 -0
- package/build/internal/ReadThroughEntityCache.js.map +1 -1
- package/build/internal/SingleFieldHolder.d.ts +7 -0
- package/build/internal/SingleFieldHolder.js +7 -0
- package/build/internal/SingleFieldHolder.js.map +1 -1
- package/build/metrics/EntityMetricsUtils.d.ts +4 -3
- package/build/metrics/EntityMetricsUtils.js +6 -3
- package/build/metrics/EntityMetricsUtils.js.map +1 -1
- package/build/metrics/IEntityMetricsAdapter.d.ts +21 -0
- package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/build/utils/EntityCreationUtils.d.ts +14 -0
- package/build/utils/EntityCreationUtils.js +57 -0
- package/build/utils/EntityCreationUtils.js.map +1 -0
- package/package.json +13 -13
- package/src/AuthorizationResultBasedEntityLoader.ts +7 -1
- package/src/AuthorizationResultBasedEntityMutator.ts +133 -15
- package/src/EntityLoaderUtils.ts +43 -12
- package/src/EntityQueryContext.ts +68 -13
- package/src/EntityQueryContextProvider.ts +20 -3
- package/src/IEntityGenericCacher.ts +2 -2
- package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +98 -0
- package/src/__tests__/EntityQueryContext-test.ts +141 -26
- package/src/errors/EntityNotFoundError.ts +51 -4
- package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +26 -0
- package/src/index.ts +1 -0
- package/src/internal/CompositeFieldHolder.ts +15 -0
- package/src/internal/CompositeFieldValueMap.ts +3 -0
- package/src/internal/EntityDataManager.ts +170 -10
- package/src/internal/EntityFieldTransformationUtils.ts +20 -0
- package/src/internal/EntityLoadInterfaces.ts +8 -0
- package/src/internal/EntityTableDataCoordinator.ts +2 -0
- package/src/internal/ReadThroughEntityCache.ts +8 -0
- package/src/internal/SingleFieldHolder.ts +7 -0
- package/src/internal/__tests__/EntityDataManager-test.ts +708 -186
- package/src/metrics/EntityMetricsUtils.ts +7 -0
- package/src/metrics/IEntityMetricsAdapter.ts +27 -0
- package/src/utils/EntityCreationUtils.ts +143 -0
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -1
- package/src/utils/__tests__/EntityCreationUtils-test.ts +354 -0
|
@@ -14,8 +14,15 @@ export enum TransactionIsolationLevel {
|
|
|
14
14
|
SERIALIZABLE = 'SERIALIZABLE',
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export enum TransactionalDataLoaderMode {
|
|
18
|
+
ENABLED = 'ENABLED',
|
|
19
|
+
ENABLED_BATCH_ONLY = 'ENABLED_BATCH_ONLY',
|
|
20
|
+
DISABLED = 'DISABLED',
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export type TransactionConfig = {
|
|
18
24
|
isolationLevel?: TransactionIsolationLevel;
|
|
25
|
+
transactionalDataLoaderMode?: TransactionalDataLoaderMode;
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
/**
|
|
@@ -28,7 +35,7 @@ export type TransactionConfig = {
|
|
|
28
35
|
export abstract class EntityQueryContext {
|
|
29
36
|
constructor(private readonly queryInterface: any) {}
|
|
30
37
|
|
|
31
|
-
abstract isInTransaction():
|
|
38
|
+
abstract isInTransaction(): this is EntityTransactionalQueryContext;
|
|
32
39
|
|
|
33
40
|
getQueryInterface(): any {
|
|
34
41
|
return this.queryInterface;
|
|
@@ -54,7 +61,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|
|
54
61
|
super(queryInterface);
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
isInTransaction():
|
|
64
|
+
override isInTransaction(): this is EntityTransactionalQueryContext {
|
|
58
65
|
return false;
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -75,6 +82,11 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|
|
75
82
|
* dependent triggers and validators will run within the transaction.
|
|
76
83
|
*/
|
|
77
84
|
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
85
|
+
/**
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
public readonly childQueryContexts: EntityNestedTransactionalQueryContext[] = [];
|
|
89
|
+
|
|
78
90
|
private readonly postCommitInvalidationCallbacks: PostCommitCallback[] = [];
|
|
79
91
|
private readonly postCommitCallbacks: PostCommitCallback[] = [];
|
|
80
92
|
|
|
@@ -83,6 +95,11 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
83
95
|
constructor(
|
|
84
96
|
queryInterface: any,
|
|
85
97
|
private readonly entityQueryContextProvider: EntityQueryContextProvider,
|
|
98
|
+
/**
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
readonly transactionId: string,
|
|
102
|
+
public readonly transactionalDataLoaderMode: TransactionalDataLoaderMode,
|
|
86
103
|
) {
|
|
87
104
|
super(queryInterface);
|
|
88
105
|
}
|
|
@@ -121,6 +138,9 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
121
138
|
this.postCommitCallbacks.push(callback);
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
/**
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
124
144
|
public async runPreCommitCallbacksAsync(): Promise<void> {
|
|
125
145
|
const callbacks = [...this.preCommitCallbacks]
|
|
126
146
|
.sort((a, b) => a.order - b.order)
|
|
@@ -132,6 +152,9 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
135
158
|
public async runPostCommitCallbacksAsync(): Promise<void> {
|
|
136
159
|
const invalidationCallbacks = [...this.postCommitInvalidationCallbacks];
|
|
137
160
|
this.postCommitInvalidationCallbacks.length = 0;
|
|
@@ -142,10 +165,14 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
|
142
165
|
await Promise.all(callbacks.map((callback) => callback()));
|
|
143
166
|
}
|
|
144
167
|
|
|
145
|
-
isInTransaction():
|
|
168
|
+
override isInTransaction(): this is EntityTransactionalQueryContext {
|
|
146
169
|
return true;
|
|
147
170
|
}
|
|
148
171
|
|
|
172
|
+
isInNestedTransaction(): this is EntityNestedTransactionalQueryContext {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
async runInTransactionIfNotInTransactionAsync<T>(
|
|
150
177
|
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<T>,
|
|
151
178
|
transactionConfig?: TransactionConfig,
|
|
@@ -181,31 +208,59 @@ export class EntityNestedTransactionalQueryContext extends EntityTransactionalQu
|
|
|
181
208
|
|
|
182
209
|
constructor(
|
|
183
210
|
queryInterface: any,
|
|
184
|
-
|
|
211
|
+
/**
|
|
212
|
+
* @internal
|
|
213
|
+
*/
|
|
214
|
+
readonly parentQueryContext: EntityTransactionalQueryContext,
|
|
185
215
|
entityQueryContextProvider: EntityQueryContextProvider,
|
|
216
|
+
transactionId: string,
|
|
217
|
+
transactionalDataLoaderMode: TransactionalDataLoaderMode,
|
|
186
218
|
) {
|
|
187
|
-
super(queryInterface, entityQueryContextProvider);
|
|
219
|
+
super(queryInterface, entityQueryContextProvider, transactionId, transactionalDataLoaderMode);
|
|
220
|
+
parentQueryContext.childQueryContexts.push(this);
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
override isInNestedTransaction(): this is EntityNestedTransactionalQueryContext {
|
|
224
|
+
return true;
|
|
192
225
|
}
|
|
193
226
|
|
|
194
|
-
public override
|
|
227
|
+
public override appendPostCommitCallback(callback: PostCommitCallback): void {
|
|
228
|
+
// explicitly do not add to the super-class's post-commit callbacks
|
|
229
|
+
// instead, we will add them to the parent transaction's post-commit callbacks
|
|
230
|
+
// after the nested transaction has been committed
|
|
195
231
|
this.postCommitCallbacksToTransfer.push(callback);
|
|
196
232
|
}
|
|
197
233
|
|
|
198
|
-
public override
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
234
|
+
public override appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
|
|
235
|
+
super.appendPostCommitInvalidationCallback(callback);
|
|
236
|
+
this.postCommitInvalidationCallbacksToTransfer.push(callback);
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
|
|
239
|
+
/**
|
|
240
|
+
* The behavior of callbacks for nested transactions are a bit different than for normal
|
|
241
|
+
* transactions.
|
|
242
|
+
* - Post-commit (non-invalidation) callbacks are run at the end of the outermost transaction
|
|
243
|
+
* since they often contain side-effects that only should run if the transaction doesn't roll back.
|
|
244
|
+
* The outermost transaction has the final say on the commit state of itself and all sub-transactions.
|
|
245
|
+
* - Invalidation callbacks are run at the end of both the nested transaction iteself but also transferred
|
|
246
|
+
* to the parent transaction to be run at the end of it (and recurse upwards, accumulating invalations).
|
|
247
|
+
* This is to ensure the dataloader cache is never stale no matter the DBMS transaction isolation
|
|
248
|
+
* semantics. See the note in `AuthorizationResultBasedBaseMutator` for more details.
|
|
249
|
+
*
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
public override async runPostCommitCallbacksAsync(): Promise<void> {
|
|
253
|
+
// run the post-commit callbacks for the nested transaction now
|
|
254
|
+
// (this technically also would run regular post-commit callbacks, but they are empty)
|
|
255
|
+
await super.runPostCommitCallbacksAsync();
|
|
256
|
+
|
|
257
|
+
// transfer a copy of the post-commit invalidation callbacks to the parent transaction
|
|
258
|
+
// to also be run at the end of it (or recurse in the case of the parent transaction being nested as well)
|
|
205
259
|
for (const callback of this.postCommitInvalidationCallbacksToTransfer) {
|
|
206
260
|
this.parentQueryContext.appendPostCommitInvalidationCallback(callback);
|
|
207
261
|
}
|
|
208
262
|
|
|
263
|
+
// transfer post-commit callbacks to patent
|
|
209
264
|
for (const callback of this.postCommitCallbacksToTransfer) {
|
|
210
265
|
this.parentQueryContext.appendPostCommitCallback(callback);
|
|
211
266
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
EntityTransactionalQueryContext,
|
|
3
5
|
EntityNonTransactionalQueryContext,
|
|
4
6
|
EntityNestedTransactionalQueryContext,
|
|
5
7
|
TransactionConfig,
|
|
8
|
+
TransactionalDataLoaderMode,
|
|
6
9
|
} from './EntityQueryContext';
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -21,6 +24,13 @@ export default abstract class EntityQueryContextProvider {
|
|
|
21
24
|
*/
|
|
22
25
|
protected abstract getQueryInterface(): any;
|
|
23
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @returns true if the transactional dataloader should be disabled for all transactions.
|
|
29
|
+
*/
|
|
30
|
+
protected defaultTransactionalDataLoaderMode(): TransactionalDataLoaderMode {
|
|
31
|
+
return TransactionalDataLoaderMode.ENABLED;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
/**
|
|
25
35
|
* Vend a transaction runner for use in runInTransactionAsync.
|
|
26
36
|
*/
|
|
@@ -43,7 +53,12 @@ export default abstract class EntityQueryContextProvider {
|
|
|
43
53
|
const [returnedValue, queryContext] = await this.createTransactionRunner<
|
|
44
54
|
[T, EntityTransactionalQueryContext]
|
|
45
55
|
>(transactionConfig)(async (queryInterface) => {
|
|
46
|
-
const queryContext = new EntityTransactionalQueryContext(
|
|
56
|
+
const queryContext = new EntityTransactionalQueryContext(
|
|
57
|
+
queryInterface,
|
|
58
|
+
this,
|
|
59
|
+
randomUUID(),
|
|
60
|
+
transactionConfig?.transactionalDataLoaderMode ?? this.defaultTransactionalDataLoaderMode(),
|
|
61
|
+
);
|
|
47
62
|
const result = await transactionScope(queryContext);
|
|
48
63
|
await queryContext.runPreCommitCallbacksAsync();
|
|
49
64
|
return [result, queryContext];
|
|
@@ -69,13 +84,15 @@ export default abstract class EntityQueryContextProvider {
|
|
|
69
84
|
innerQueryInterface,
|
|
70
85
|
outerQueryContext,
|
|
71
86
|
this,
|
|
87
|
+
randomUUID(),
|
|
88
|
+
outerQueryContext.transactionalDataLoaderMode,
|
|
72
89
|
);
|
|
73
90
|
const result = await transactionScope(innerQueryContext);
|
|
74
91
|
await innerQueryContext.runPreCommitCallbacksAsync();
|
|
75
92
|
return [result, innerQueryContext];
|
|
76
93
|
});
|
|
77
|
-
//
|
|
78
|
-
innerQueryContext.
|
|
94
|
+
// behavior of this call differs for nested transaction query contexts from regular transaction query contexts
|
|
95
|
+
await innerQueryContext.runPostCommitCallbacksAsync();
|
|
79
96
|
return returnedValue;
|
|
80
97
|
}
|
|
81
98
|
}
|
|
@@ -59,8 +59,8 @@ export default interface IEntityGenericCacher<
|
|
|
59
59
|
* from makeCacheKeyForStorage because invalidation can optionally be configured to invalidate a larger set of keys than
|
|
60
60
|
* the one for just the current cache version, which can be useful for things like push safety.
|
|
61
61
|
*
|
|
62
|
-
* @param key - load key
|
|
63
|
-
* @param
|
|
62
|
+
* @param key - load key for the cache keys
|
|
63
|
+
* @param value - load value for the cache keys
|
|
64
64
|
*/
|
|
65
65
|
makeCacheKeysForInvalidation<
|
|
66
66
|
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
|
@@ -890,6 +890,104 @@ describe(AuthorizationResultBasedEntityLoader, () => {
|
|
|
890
890
|
).once();
|
|
891
891
|
});
|
|
892
892
|
|
|
893
|
+
it('invalidates upon invalidate by entity within transaction', async () => {
|
|
894
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
895
|
+
const privacyPolicyEvaluationContext =
|
|
896
|
+
instance(
|
|
897
|
+
mock<
|
|
898
|
+
EntityPrivacyPolicyEvaluationContext<
|
|
899
|
+
TestFields,
|
|
900
|
+
'customIdField',
|
|
901
|
+
ViewerContext,
|
|
902
|
+
TestEntity
|
|
903
|
+
>
|
|
904
|
+
>(),
|
|
905
|
+
);
|
|
906
|
+
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
|
|
907
|
+
|
|
908
|
+
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
|
|
909
|
+
const dataManagerMock = mock<EntityDataManager<TestFields, 'customIdField'>>();
|
|
910
|
+
const dataManagerInstance = instance(dataManagerMock);
|
|
911
|
+
|
|
912
|
+
const id1 = uuidv4();
|
|
913
|
+
const entityMock = mock(TestEntity);
|
|
914
|
+
const date = new Date();
|
|
915
|
+
|
|
916
|
+
when(entityMock.getAllDatabaseFields()).thenReturn({
|
|
917
|
+
customIdField: id1,
|
|
918
|
+
testIndexedField: 'h1',
|
|
919
|
+
intField: 5,
|
|
920
|
+
stringField: 'huh',
|
|
921
|
+
dateField: date,
|
|
922
|
+
nullableField: null,
|
|
923
|
+
});
|
|
924
|
+
const entityInstance = instance(entityMock);
|
|
925
|
+
|
|
926
|
+
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
|
|
927
|
+
const utils = new EntityLoaderUtils(
|
|
928
|
+
viewerContext,
|
|
929
|
+
queryContext,
|
|
930
|
+
privacyPolicyEvaluationContext,
|
|
931
|
+
testEntityConfiguration,
|
|
932
|
+
TestEntity,
|
|
933
|
+
/* entitySelectedFields */ undefined,
|
|
934
|
+
privacyPolicy,
|
|
935
|
+
dataManagerInstance,
|
|
936
|
+
metricsAdapter,
|
|
937
|
+
);
|
|
938
|
+
const entityLoader = new AuthorizationResultBasedEntityLoader(
|
|
939
|
+
queryContext,
|
|
940
|
+
testEntityConfiguration,
|
|
941
|
+
TestEntity,
|
|
942
|
+
dataManagerInstance,
|
|
943
|
+
metricsAdapter,
|
|
944
|
+
utils,
|
|
945
|
+
);
|
|
946
|
+
entityLoader.utils.invalidateEntityForTransaction(queryContext, entityInstance);
|
|
947
|
+
|
|
948
|
+
verify(
|
|
949
|
+
dataManagerMock.invalidateKeyValuePairsForTransaction(queryContext, anything()),
|
|
950
|
+
).once();
|
|
951
|
+
verify(
|
|
952
|
+
dataManagerMock.invalidateKeyValuePairsForTransaction(
|
|
953
|
+
queryContext,
|
|
954
|
+
deepEqualEntityAware([
|
|
955
|
+
[
|
|
956
|
+
new SingleFieldHolder<TestFields, 'customIdField', 'customIdField'>('customIdField'),
|
|
957
|
+
new SingleFieldValueHolder<TestFields, 'customIdField'>(id1),
|
|
958
|
+
],
|
|
959
|
+
[
|
|
960
|
+
new SingleFieldHolder<TestFields, 'customIdField', 'testIndexedField'>(
|
|
961
|
+
'testIndexedField',
|
|
962
|
+
),
|
|
963
|
+
new SingleFieldValueHolder<TestFields, 'testIndexedField'>('h1'),
|
|
964
|
+
],
|
|
965
|
+
[
|
|
966
|
+
new SingleFieldHolder<TestFields, 'customIdField', 'intField'>('intField'),
|
|
967
|
+
new SingleFieldValueHolder<TestFields, 'intField'>(5),
|
|
968
|
+
],
|
|
969
|
+
[
|
|
970
|
+
new SingleFieldHolder<TestFields, 'customIdField', 'stringField'>('stringField'),
|
|
971
|
+
new SingleFieldValueHolder<TestFields, 'stringField'>('huh'),
|
|
972
|
+
],
|
|
973
|
+
[
|
|
974
|
+
new SingleFieldHolder<TestFields, 'customIdField', 'dateField'>('dateField'),
|
|
975
|
+
new SingleFieldValueHolder<TestFields, 'dateField'>(date),
|
|
976
|
+
],
|
|
977
|
+
[
|
|
978
|
+
new CompositeFieldHolder(['stringField', 'intField']),
|
|
979
|
+
new CompositeFieldValueHolder({ stringField: 'huh', intField: 5 }),
|
|
980
|
+
],
|
|
981
|
+
[
|
|
982
|
+
new CompositeFieldHolder(['stringField', 'testIndexedField']),
|
|
983
|
+
new CompositeFieldValueHolder({ stringField: 'huh', testIndexedField: 'h1' }),
|
|
984
|
+
],
|
|
985
|
+
]),
|
|
986
|
+
),
|
|
987
|
+
).once();
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
|
|
893
991
|
it('returns error result when not allowed', async () => {
|
|
894
992
|
const viewerContext = instance(mock(ViewerContext));
|
|
895
993
|
const privacyPolicyEvaluationContext =
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
1
2
|
import invariant from 'invariant';
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import EntityCompanionProvider from '../EntityCompanionProvider';
|
|
5
|
+
import {
|
|
6
|
+
EntityQueryContext,
|
|
7
|
+
TransactionalDataLoaderMode,
|
|
8
|
+
TransactionConfig,
|
|
9
|
+
TransactionIsolationLevel,
|
|
10
|
+
} from '../EntityQueryContext';
|
|
11
|
+
import EntityQueryContextProvider from '../EntityQueryContextProvider';
|
|
4
12
|
import ViewerContext from '../ViewerContext';
|
|
13
|
+
import NoOpEntityMetricsAdapter from '../metrics/NoOpEntityMetricsAdapter';
|
|
14
|
+
import { InMemoryFullCacheStubCacheAdapterProvider } from '../utils/__testfixtures__/StubCacheAdapter';
|
|
15
|
+
import StubDatabaseAdapterProvider from '../utils/__testfixtures__/StubDatabaseAdapterProvider';
|
|
5
16
|
import { createUnitTestEntityCompanionProvider } from '../utils/__testfixtures__/createUnitTestEntityCompanionProvider';
|
|
6
17
|
|
|
7
18
|
describe(EntityQueryContext, () => {
|
|
@@ -89,7 +100,9 @@ describe(EntityQueryContext, () => {
|
|
|
89
100
|
throw new Error('wat');
|
|
90
101
|
});
|
|
91
102
|
const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {});
|
|
103
|
+
const postCommitNestedInvalidationCallback = jest.fn(async (): Promise<void> => {});
|
|
92
104
|
const postCommitCallback = jest.fn(async (): Promise<void> => {});
|
|
105
|
+
const postCommitNestedCallback = jest.fn(async (): Promise<void> => {});
|
|
93
106
|
|
|
94
107
|
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
95
108
|
'postgres',
|
|
@@ -100,19 +113,19 @@ describe(EntityQueryContext, () => {
|
|
|
100
113
|
|
|
101
114
|
await Promise.all([
|
|
102
115
|
queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
103
|
-
innerQueryContext.appendPostCommitCallback(
|
|
116
|
+
innerQueryContext.appendPostCommitCallback(postCommitNestedCallback);
|
|
104
117
|
innerQueryContext.appendPostCommitInvalidationCallback(
|
|
105
|
-
|
|
118
|
+
postCommitNestedInvalidationCallback,
|
|
106
119
|
);
|
|
107
120
|
innerQueryContext.appendPreCommitCallback(preCommitNestedCallback, 0);
|
|
108
121
|
}),
|
|
109
122
|
(async () => {
|
|
110
123
|
try {
|
|
111
124
|
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
112
|
-
// these two shouldn't be called
|
|
113
|
-
innerQueryContext.appendPostCommitCallback(
|
|
125
|
+
// these two shouldn't be called due to throwing pre-commit callback
|
|
126
|
+
innerQueryContext.appendPostCommitCallback(postCommitNestedCallback);
|
|
114
127
|
innerQueryContext.appendPostCommitInvalidationCallback(
|
|
115
|
-
|
|
128
|
+
postCommitNestedInvalidationCallback,
|
|
116
129
|
);
|
|
117
130
|
innerQueryContext.appendPreCommitCallback(preCommitNestedCallbackThrow, 0);
|
|
118
131
|
});
|
|
@@ -125,26 +138,10 @@ describe(EntityQueryContext, () => {
|
|
|
125
138
|
expect(preCommitCallback).toHaveBeenCalledTimes(1);
|
|
126
139
|
expect(preCommitNestedCallback).toHaveBeenCalledTimes(1);
|
|
127
140
|
expect(preCommitNestedCallbackThrow).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(postCommitCallback).toHaveBeenCalledTimes(
|
|
129
|
-
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
it('does not support calling runPostCommitCallbacksAsync on nested transaction', async () => {
|
|
133
|
-
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
134
|
-
const viewerContext = new ViewerContext(companionProvider);
|
|
135
|
-
|
|
136
|
-
await expect(
|
|
137
|
-
viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
138
|
-
'postgres',
|
|
139
|
-
async (queryContext) => {
|
|
140
|
-
await queryContext.runInNestedTransactionAsync(async (innerQueryContext) => {
|
|
141
|
-
await innerQueryContext.runPostCommitCallbacksAsync();
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
),
|
|
145
|
-
).rejects.toThrowError(
|
|
146
|
-
'Must not call runPostCommitCallbacksAsync on EntityNestedTransactionalQueryContext',
|
|
147
|
-
);
|
|
141
|
+
expect(postCommitCallback).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(postCommitNestedInvalidationCallback).toHaveBeenCalledTimes(2);
|
|
144
|
+
expect(postCommitNestedCallback).toHaveBeenCalledTimes(1);
|
|
148
145
|
});
|
|
149
146
|
});
|
|
150
147
|
|
|
@@ -168,5 +165,123 @@ describe(EntityQueryContext, () => {
|
|
|
168
165
|
|
|
169
166
|
expect(queryContextProviderSpy).toHaveBeenCalledWith(transactionScopeFn, transactionConfig);
|
|
170
167
|
});
|
|
168
|
+
|
|
169
|
+
it('makes the result query context enable/disable transactional dataloaders', async () => {
|
|
170
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
171
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
172
|
+
|
|
173
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
174
|
+
'postgres',
|
|
175
|
+
async (queryContext) => {
|
|
176
|
+
assert(queryContext.isInTransaction());
|
|
177
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
178
|
+
TransactionalDataLoaderMode.DISABLED,
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
{ transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
185
|
+
'postgres',
|
|
186
|
+
async (queryContext) => {
|
|
187
|
+
assert(queryContext.isInTransaction());
|
|
188
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
189
|
+
TransactionalDataLoaderMode.ENABLED_BATCH_ONLY,
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
{ transactionalDataLoaderMode: TransactionalDataLoaderMode.ENABLED_BATCH_ONLY },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
196
|
+
'postgres',
|
|
197
|
+
async (queryContext) => {
|
|
198
|
+
assert(queryContext.isInTransaction());
|
|
199
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
200
|
+
TransactionalDataLoaderMode.ENABLED,
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('global defaultTransactionalDataLoaderMode', () => {
|
|
208
|
+
class StubQueryContextProviderWithDisabledTransactionalDataLoaders extends EntityQueryContextProvider {
|
|
209
|
+
protected getQueryInterface(): any {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected override defaultTransactionalDataLoaderMode(): TransactionalDataLoaderMode {
|
|
214
|
+
return TransactionalDataLoaderMode.DISABLED;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
protected createTransactionRunner<T>(
|
|
218
|
+
_transactionConfig?: TransactionConfig,
|
|
219
|
+
): (transactionScope: (queryInterface: any) => Promise<T>) => Promise<T> {
|
|
220
|
+
return (transactionScope) => Promise.resolve(transactionScope({}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
protected createNestedTransactionRunner<T>(
|
|
224
|
+
_outerQueryInterface: any,
|
|
225
|
+
): (transactionScope: (queryInterface: any) => Promise<T>) => Promise<T> {
|
|
226
|
+
return (transactionScope) => Promise.resolve(transactionScope({}));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
it('respects the global setting but allows overriding by transaction config', async () => {
|
|
231
|
+
const companionProvider = new EntityCompanionProvider(
|
|
232
|
+
new NoOpEntityMetricsAdapter(),
|
|
233
|
+
new Map([
|
|
234
|
+
[
|
|
235
|
+
'postgres',
|
|
236
|
+
{
|
|
237
|
+
adapterProvider: new StubDatabaseAdapterProvider(),
|
|
238
|
+
queryContextProvider:
|
|
239
|
+
new StubQueryContextProviderWithDisabledTransactionalDataLoaders(),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
]),
|
|
243
|
+
new Map([
|
|
244
|
+
[
|
|
245
|
+
'redis',
|
|
246
|
+
{
|
|
247
|
+
cacheAdapterProvider: new InMemoryFullCacheStubCacheAdapterProvider(),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
]),
|
|
251
|
+
);
|
|
252
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
253
|
+
|
|
254
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
255
|
+
'postgres',
|
|
256
|
+
async (queryContext) => {
|
|
257
|
+
assert(queryContext.isInTransaction());
|
|
258
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
259
|
+
TransactionalDataLoaderMode.DISABLED,
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
{ transactionalDataLoaderMode: TransactionalDataLoaderMode.DISABLED },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
266
|
+
'postgres',
|
|
267
|
+
async (queryContext) => {
|
|
268
|
+
assert(queryContext.isInTransaction());
|
|
269
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
270
|
+
TransactionalDataLoaderMode.ENABLED_BATCH_ONLY,
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
{ transactionalDataLoaderMode: TransactionalDataLoaderMode.ENABLED_BATCH_ONLY },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
277
|
+
'postgres',
|
|
278
|
+
async (queryContext) => {
|
|
279
|
+
assert(queryContext.isInTransaction());
|
|
280
|
+
expect(queryContext.transactionalDataLoaderMode).toBe(
|
|
281
|
+
TransactionalDataLoaderMode.DISABLED,
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
});
|
|
171
286
|
});
|
|
172
287
|
});
|
|
@@ -4,6 +4,33 @@ import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
|
|
4
4
|
import ReadonlyEntity from '../ReadonlyEntity';
|
|
5
5
|
import ViewerContext from '../ViewerContext';
|
|
6
6
|
|
|
7
|
+
type EntityNotFoundOptions<
|
|
8
|
+
TFields extends Record<string, any>,
|
|
9
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
10
|
+
TViewerContext extends ViewerContext,
|
|
11
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
12
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
13
|
+
TFields,
|
|
14
|
+
TIDField,
|
|
15
|
+
TViewerContext,
|
|
16
|
+
TEntity,
|
|
17
|
+
TSelectedFields
|
|
18
|
+
>,
|
|
19
|
+
N extends keyof TFields,
|
|
20
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
21
|
+
> = {
|
|
22
|
+
entityClass: IEntityClass<
|
|
23
|
+
TFields,
|
|
24
|
+
TIDField,
|
|
25
|
+
TViewerContext,
|
|
26
|
+
TEntity,
|
|
27
|
+
TPrivacyPolicy,
|
|
28
|
+
TSelectedFields
|
|
29
|
+
>;
|
|
30
|
+
fieldName: N;
|
|
31
|
+
fieldValue: TFields[N];
|
|
32
|
+
};
|
|
33
|
+
|
|
7
34
|
export default class EntityNotFoundError<
|
|
8
35
|
TFields extends Record<string, any>,
|
|
9
36
|
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
@@ -22,18 +49,38 @@ export default class EntityNotFoundError<
|
|
|
22
49
|
public readonly state = EntityErrorState.PERMANENT;
|
|
23
50
|
public readonly code = EntityErrorCode.ERR_ENTITY_NOT_FOUND;
|
|
24
51
|
|
|
52
|
+
constructor(message: string);
|
|
25
53
|
constructor(
|
|
26
|
-
|
|
54
|
+
options: EntityNotFoundOptions<
|
|
27
55
|
TFields,
|
|
28
56
|
TIDField,
|
|
29
57
|
TViewerContext,
|
|
30
58
|
TEntity,
|
|
31
59
|
TPrivacyPolicy,
|
|
60
|
+
N,
|
|
32
61
|
TSelectedFields
|
|
33
62
|
>,
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
messageOrOptions:
|
|
67
|
+
| string
|
|
68
|
+
| EntityNotFoundOptions<
|
|
69
|
+
TFields,
|
|
70
|
+
TIDField,
|
|
71
|
+
TViewerContext,
|
|
72
|
+
TEntity,
|
|
73
|
+
TPrivacyPolicy,
|
|
74
|
+
N,
|
|
75
|
+
TSelectedFields
|
|
76
|
+
>,
|
|
36
77
|
) {
|
|
37
|
-
|
|
78
|
+
if (typeof messageOrOptions === 'string') {
|
|
79
|
+
super(messageOrOptions);
|
|
80
|
+
} else {
|
|
81
|
+
super(
|
|
82
|
+
`Entity not found: ${messageOrOptions.entityClass.name} (${String(messageOrOptions.fieldName)} = ${messageOrOptions.fieldValue})`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
38
85
|
}
|
|
39
86
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import EntityDatabaseAdapterError, {
|
|
2
|
+
EntityDatabaseAdapterTransientError,
|
|
3
|
+
EntityDatabaseAdapterUnknownError,
|
|
4
|
+
EntityDatabaseAdapterCheckConstraintError,
|
|
5
|
+
EntityDatabaseAdapterExclusionConstraintError,
|
|
6
|
+
EntityDatabaseAdapterForeignKeyConstraintError,
|
|
7
|
+
EntityDatabaseAdapterNotNullConstraintError,
|
|
8
|
+
EntityDatabaseAdapterUniqueConstraintError,
|
|
9
|
+
} from '../EntityDatabaseAdapterError';
|
|
10
|
+
|
|
11
|
+
describe(EntityDatabaseAdapterError, () => {
|
|
12
|
+
// necessary for coverage within the entity package since these errors are
|
|
13
|
+
// currently only ever instantiated by database adapter implementations
|
|
14
|
+
it('instantiates all errors successfully', () => {
|
|
15
|
+
const errors = [
|
|
16
|
+
new EntityDatabaseAdapterTransientError('test'),
|
|
17
|
+
new EntityDatabaseAdapterUnknownError('test'),
|
|
18
|
+
new EntityDatabaseAdapterCheckConstraintError('test'),
|
|
19
|
+
new EntityDatabaseAdapterExclusionConstraintError('test'),
|
|
20
|
+
new EntityDatabaseAdapterForeignKeyConstraintError('test'),
|
|
21
|
+
new EntityDatabaseAdapterNotNullConstraintError('test'),
|
|
22
|
+
new EntityDatabaseAdapterUniqueConstraintError('test'),
|
|
23
|
+
];
|
|
24
|
+
expect(errors).not.toBeFalsy();
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -85,6 +85,7 @@ export { default as AlwaysDenyPrivacyPolicyRule } from './rules/AlwaysDenyPrivac
|
|
|
85
85
|
export { default as AlwaysSkipPrivacyPolicyRule } from './rules/AlwaysSkipPrivacyPolicyRule';
|
|
86
86
|
export { default as PrivacyPolicyRule } from './rules/PrivacyPolicyRule';
|
|
87
87
|
export * from './rules/PrivacyPolicyRule';
|
|
88
|
+
export * from './utils/EntityCreationUtils';
|
|
88
89
|
export * from './utils/EntityPrivacyUtils';
|
|
89
90
|
export * from './utils/mergeEntityMutationTriggerConfigurations';
|
|
90
91
|
export * from './utils/collections/maps';
|