@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
|
@@ -2,6 +2,7 @@ import IEntityMetricsAdapter, {
|
|
|
2
2
|
EntityMetricsLoadType,
|
|
3
3
|
EntityMetricsMutationType,
|
|
4
4
|
} from './IEntityMetricsAdapter';
|
|
5
|
+
import { EntityQueryContext } from '../EntityQueryContext';
|
|
5
6
|
import { IEntityLoadValue } from '../internal/EntityLoadInterfaces';
|
|
6
7
|
import { reduceMap } from '../utils/collections/maps';
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ export const timeAndLogLoadEventAsync =
|
|
|
10
11
|
metricsAdapter: IEntityMetricsAdapter,
|
|
11
12
|
loadType: EntityMetricsLoadType,
|
|
12
13
|
entityClassName: string,
|
|
14
|
+
queryContext: EntityQueryContext,
|
|
13
15
|
) =>
|
|
14
16
|
async <TFields>(promise: Promise<readonly Readonly<TFields>[]>) => {
|
|
15
17
|
const startTime = Date.now();
|
|
@@ -18,6 +20,7 @@ export const timeAndLogLoadEventAsync =
|
|
|
18
20
|
|
|
19
21
|
metricsAdapter.logDataManagerLoadEvent({
|
|
20
22
|
type: loadType,
|
|
23
|
+
isInTransaction: queryContext.isInTransaction(),
|
|
21
24
|
entityClassName,
|
|
22
25
|
duration: endTime - startTime,
|
|
23
26
|
count: result.length,
|
|
@@ -31,6 +34,7 @@ export const timeAndLogLoadMapEventAsync =
|
|
|
31
34
|
metricsAdapter: IEntityMetricsAdapter,
|
|
32
35
|
loadType: EntityMetricsLoadType,
|
|
33
36
|
entityClassName: string,
|
|
37
|
+
queryContext: EntityQueryContext,
|
|
34
38
|
) =>
|
|
35
39
|
async <
|
|
36
40
|
TFields extends Record<string, any>,
|
|
@@ -47,6 +51,7 @@ export const timeAndLogLoadMapEventAsync =
|
|
|
47
51
|
|
|
48
52
|
metricsAdapter.logDataManagerLoadEvent({
|
|
49
53
|
type: loadType,
|
|
54
|
+
isInTransaction: queryContext.isInTransaction(),
|
|
50
55
|
entityClassName,
|
|
51
56
|
duration: endTime - startTime,
|
|
52
57
|
count,
|
|
@@ -60,6 +65,7 @@ export const timeAndLogMutationEventAsync =
|
|
|
60
65
|
metricsAdapter: IEntityMetricsAdapter,
|
|
61
66
|
mutationType: EntityMetricsMutationType,
|
|
62
67
|
entityClassName: string,
|
|
68
|
+
queryContext: EntityQueryContext,
|
|
63
69
|
) =>
|
|
64
70
|
async <T>(promise: Promise<T>) => {
|
|
65
71
|
const startTime = Date.now();
|
|
@@ -68,6 +74,7 @@ export const timeAndLogMutationEventAsync =
|
|
|
68
74
|
|
|
69
75
|
metricsAdapter.logMutatorMutationEvent({
|
|
70
76
|
type: mutationType,
|
|
77
|
+
isInTransaction: queryContext.isInTransaction(),
|
|
71
78
|
entityClassName,
|
|
72
79
|
duration: endTime - startTime,
|
|
73
80
|
});
|
|
@@ -19,6 +19,11 @@ export interface EntityMetricsLoadEvent {
|
|
|
19
19
|
*/
|
|
20
20
|
type: EntityMetricsLoadType;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Whether this load is within a transaction.
|
|
24
|
+
*/
|
|
25
|
+
isInTransaction: boolean;
|
|
26
|
+
|
|
22
27
|
/**
|
|
23
28
|
* Class name of the Entity being loaded.
|
|
24
29
|
*/
|
|
@@ -47,6 +52,11 @@ export interface EntityMetricsMutationEvent {
|
|
|
47
52
|
*/
|
|
48
53
|
type: EntityMetricsMutationType;
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Whether this mutation is within a transaction.
|
|
57
|
+
*/
|
|
58
|
+
isInTransaction: boolean;
|
|
59
|
+
|
|
50
60
|
/**
|
|
51
61
|
* Class name of the Entity being mutated.
|
|
52
62
|
*/
|
|
@@ -85,6 +95,11 @@ export interface IncrementLoadCountEvent {
|
|
|
85
95
|
*/
|
|
86
96
|
type: IncrementLoadCountEventType;
|
|
87
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Whether this load is within a transaction.
|
|
100
|
+
*/
|
|
101
|
+
isInTransaction: boolean;
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* Load method type for this event.
|
|
90
105
|
*/
|
|
@@ -114,8 +129,20 @@ export interface EntityMetricsAuthorizationEvent {
|
|
|
114
129
|
* Class name of the Entity being authorized.
|
|
115
130
|
*/
|
|
116
131
|
entityClassName: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The action being authorized.
|
|
135
|
+
*/
|
|
117
136
|
action: EntityAuthorizationAction;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The result of the authorization.
|
|
140
|
+
*/
|
|
118
141
|
evaluationResult: EntityMetricsAuthorizationResult;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The evaluation mode of the privacy policy.
|
|
145
|
+
*/
|
|
119
146
|
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode;
|
|
120
147
|
}
|
|
121
148
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { IEntityClass } from '../Entity';
|
|
2
|
+
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
|
3
|
+
import { EntityTransactionalQueryContext } from '../EntityQueryContext';
|
|
4
|
+
import ReadonlyEntity from '../ReadonlyEntity';
|
|
5
|
+
import ViewerContext from '../ViewerContext';
|
|
6
|
+
import { EntityDatabaseAdapterUniqueConstraintError } from '../errors/EntityDatabaseAdapterError';
|
|
7
|
+
import EntityNotFoundError from '../errors/EntityNotFoundError';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an entity if it doesn't exist, or get the existing entity if it does.
|
|
11
|
+
*/
|
|
12
|
+
export async function createOrGetExistingAsync<
|
|
13
|
+
TFields extends object,
|
|
14
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
15
|
+
TViewerContext extends ViewerContext,
|
|
16
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
17
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
18
|
+
TFields,
|
|
19
|
+
TIDField,
|
|
20
|
+
TViewerContext,
|
|
21
|
+
TEntity,
|
|
22
|
+
TSelectedFields
|
|
23
|
+
>,
|
|
24
|
+
TGetArgs,
|
|
25
|
+
TCreateArgs,
|
|
26
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
27
|
+
>(
|
|
28
|
+
viewerContext: TViewerContext,
|
|
29
|
+
entityClass: IEntityClass<
|
|
30
|
+
TFields,
|
|
31
|
+
TIDField,
|
|
32
|
+
TViewerContext,
|
|
33
|
+
TEntity,
|
|
34
|
+
TPrivacyPolicy,
|
|
35
|
+
TSelectedFields
|
|
36
|
+
>,
|
|
37
|
+
getAsync: (
|
|
38
|
+
viewerContext: TViewerContext,
|
|
39
|
+
getArgs: TGetArgs,
|
|
40
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
41
|
+
) => Promise<TEntity | null>,
|
|
42
|
+
getArgs: TGetArgs,
|
|
43
|
+
createAsync: (
|
|
44
|
+
viewerContext: TViewerContext,
|
|
45
|
+
createArgs: TCreateArgs,
|
|
46
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
47
|
+
) => Promise<TEntity>,
|
|
48
|
+
createArgs: TCreateArgs,
|
|
49
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
50
|
+
): Promise<TEntity> {
|
|
51
|
+
if (!queryContext) {
|
|
52
|
+
const maybeEntity = await getAsync(viewerContext, getArgs);
|
|
53
|
+
if (maybeEntity) {
|
|
54
|
+
return maybeEntity;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// This is done in a nested transaction since entity may negatively cache load results per-transaction (when configured).
|
|
58
|
+
// Without it, it would
|
|
59
|
+
// 1. load the entity in the current query context, negatively cache it
|
|
60
|
+
// 2. then try to create it in the nested transaction, which may fail due to a unique constraint error
|
|
61
|
+
// 3. then try to load the entity again in the current query context, which would return null due to negative cache
|
|
62
|
+
const maybeEntity = await queryContext.runInNestedTransactionAsync((nestedQueryContext) =>
|
|
63
|
+
getAsync(viewerContext, getArgs, nestedQueryContext),
|
|
64
|
+
);
|
|
65
|
+
if (maybeEntity) {
|
|
66
|
+
return maybeEntity;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
70
|
+
viewerContext,
|
|
71
|
+
entityClass,
|
|
72
|
+
getAsync,
|
|
73
|
+
getArgs,
|
|
74
|
+
createAsync,
|
|
75
|
+
createArgs,
|
|
76
|
+
queryContext,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Account for concurrent requests that may try to create the same entity.
|
|
82
|
+
* Return the existing entity if we get a Unique Constraint error.
|
|
83
|
+
*/
|
|
84
|
+
export async function createWithUniqueConstraintRecoveryAsync<
|
|
85
|
+
TFields extends object,
|
|
86
|
+
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
|
|
87
|
+
TViewerContext extends ViewerContext,
|
|
88
|
+
TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
|
|
89
|
+
TPrivacyPolicy extends EntityPrivacyPolicy<
|
|
90
|
+
TFields,
|
|
91
|
+
TIDField,
|
|
92
|
+
TViewerContext,
|
|
93
|
+
TEntity,
|
|
94
|
+
TSelectedFields
|
|
95
|
+
>,
|
|
96
|
+
TGetArgs,
|
|
97
|
+
TCreateArgs,
|
|
98
|
+
TSelectedFields extends keyof TFields = keyof TFields,
|
|
99
|
+
>(
|
|
100
|
+
viewerContext: TViewerContext,
|
|
101
|
+
entityClass: IEntityClass<
|
|
102
|
+
TFields,
|
|
103
|
+
TIDField,
|
|
104
|
+
TViewerContext,
|
|
105
|
+
TEntity,
|
|
106
|
+
TPrivacyPolicy,
|
|
107
|
+
TSelectedFields
|
|
108
|
+
>,
|
|
109
|
+
getAsync: (
|
|
110
|
+
viewerContext: TViewerContext,
|
|
111
|
+
getArgs: TGetArgs,
|
|
112
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
113
|
+
) => Promise<TEntity | null>,
|
|
114
|
+
getArgs: TGetArgs,
|
|
115
|
+
createAsync: (
|
|
116
|
+
viewerContext: TViewerContext,
|
|
117
|
+
createArgs: TCreateArgs,
|
|
118
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
119
|
+
) => Promise<TEntity>,
|
|
120
|
+
createArgs: TCreateArgs,
|
|
121
|
+
queryContext?: EntityTransactionalQueryContext,
|
|
122
|
+
): Promise<TEntity> {
|
|
123
|
+
try {
|
|
124
|
+
if (!queryContext) {
|
|
125
|
+
return await createAsync(viewerContext, createArgs);
|
|
126
|
+
}
|
|
127
|
+
return await queryContext.runInNestedTransactionAsync((nestedQueryContext) =>
|
|
128
|
+
createAsync(viewerContext, createArgs, nestedQueryContext),
|
|
129
|
+
);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (e instanceof EntityDatabaseAdapterUniqueConstraintError) {
|
|
132
|
+
const entity = await getAsync(viewerContext, getArgs, queryContext);
|
|
133
|
+
if (!entity) {
|
|
134
|
+
throw new EntityNotFoundError(
|
|
135
|
+
`Expected entity to exist after unique constraint error: ${entityClass.name}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return entity;
|
|
139
|
+
} else {
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -49,6 +49,18 @@ export default class StubDatabaseAdapter<
|
|
|
49
49
|
return new Map();
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
private static uniqBy<T>(a: T[], keyExtractor: (k: T) => string): T[] {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
return a.filter((item) => {
|
|
55
|
+
const k = keyExtractor(item);
|
|
56
|
+
if (seen.has(k)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
seen.add(k);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
protected async fetchManyWhereInternalAsync(
|
|
53
65
|
_queryInterface: any,
|
|
54
66
|
tableName: string,
|
|
@@ -56,7 +68,7 @@ export default class StubDatabaseAdapter<
|
|
|
56
68
|
tableTuples: (readonly any[])[],
|
|
57
69
|
): Promise<object[]> {
|
|
58
70
|
const objectCollection = this.getObjectCollectionForTable(tableName);
|
|
59
|
-
const results = tableTuples.reduce(
|
|
71
|
+
const results = StubDatabaseAdapter.uniqBy(tableTuples, (tuple) => tuple.join(':')).reduce(
|
|
60
72
|
(acc, tableTuple) => {
|
|
61
73
|
return acc.concat(
|
|
62
74
|
objectCollection.filter((obj) => {
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { EntityTransactionalQueryContext } from '../../EntityQueryContext';
|
|
2
|
+
import ViewerContext from '../../ViewerContext';
|
|
3
|
+
import { EntityDatabaseAdapterUniqueConstraintError } from '../../errors/EntityDatabaseAdapterError';
|
|
4
|
+
import EntityNotFoundError from '../../errors/EntityNotFoundError';
|
|
5
|
+
import {
|
|
6
|
+
createOrGetExistingAsync,
|
|
7
|
+
createWithUniqueConstraintRecoveryAsync,
|
|
8
|
+
} from '../EntityCreationUtils';
|
|
9
|
+
import SimpleTestEntity from '../__testfixtures__/SimpleTestEntity';
|
|
10
|
+
import { createUnitTestEntityCompanionProvider } from '../__testfixtures__/createUnitTestEntityCompanionProvider';
|
|
11
|
+
|
|
12
|
+
type TArgs = object;
|
|
13
|
+
|
|
14
|
+
describe.each([true, false])('in transaction %p', (inTransaction) => {
|
|
15
|
+
describe(createOrGetExistingAsync, () => {
|
|
16
|
+
it('does not create when already exists', async () => {
|
|
17
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
18
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
19
|
+
|
|
20
|
+
const entity = await SimpleTestEntity.creator(viewerContext).createAsync();
|
|
21
|
+
|
|
22
|
+
const args: TArgs = {};
|
|
23
|
+
|
|
24
|
+
const getFn = jest.fn(
|
|
25
|
+
async (
|
|
26
|
+
_vc: ViewerContext,
|
|
27
|
+
_args: TArgs,
|
|
28
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
29
|
+
) => {
|
|
30
|
+
return entity;
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const createFn = jest.fn(
|
|
35
|
+
async (vc: ViewerContext, _args: TArgs, queryContext?: EntityTransactionalQueryContext) => {
|
|
36
|
+
return await SimpleTestEntity.creator(vc, queryContext).createAsync();
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (inTransaction) {
|
|
41
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
42
|
+
'postgres',
|
|
43
|
+
async (queryContext) => {
|
|
44
|
+
await createOrGetExistingAsync(
|
|
45
|
+
viewerContext,
|
|
46
|
+
SimpleTestEntity,
|
|
47
|
+
getFn,
|
|
48
|
+
args,
|
|
49
|
+
createFn,
|
|
50
|
+
args,
|
|
51
|
+
queryContext,
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
await createOrGetExistingAsync(
|
|
57
|
+
viewerContext,
|
|
58
|
+
SimpleTestEntity,
|
|
59
|
+
getFn,
|
|
60
|
+
args,
|
|
61
|
+
createFn,
|
|
62
|
+
args,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(getFn).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(createFn).toHaveBeenCalledTimes(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('creates when not found', async () => {
|
|
71
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
72
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
73
|
+
|
|
74
|
+
const args: TArgs = {};
|
|
75
|
+
|
|
76
|
+
const getFn = jest.fn(
|
|
77
|
+
async (
|
|
78
|
+
_vc: ViewerContext,
|
|
79
|
+
_args: TArgs,
|
|
80
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
81
|
+
) => {
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const createFn = jest.fn(
|
|
87
|
+
async (vc: ViewerContext, _args: TArgs, queryContext?: EntityTransactionalQueryContext) => {
|
|
88
|
+
return await SimpleTestEntity.creator(vc, queryContext).createAsync();
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (inTransaction) {
|
|
93
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
94
|
+
'postgres',
|
|
95
|
+
async (queryContext) => {
|
|
96
|
+
await createOrGetExistingAsync(
|
|
97
|
+
viewerContext,
|
|
98
|
+
SimpleTestEntity,
|
|
99
|
+
getFn,
|
|
100
|
+
args,
|
|
101
|
+
createFn,
|
|
102
|
+
args,
|
|
103
|
+
queryContext,
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
await createOrGetExistingAsync(
|
|
109
|
+
viewerContext,
|
|
110
|
+
SimpleTestEntity,
|
|
111
|
+
getFn,
|
|
112
|
+
args,
|
|
113
|
+
createFn,
|
|
114
|
+
args,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(getFn).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(createFn).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe(createWithUniqueConstraintRecoveryAsync, () => {
|
|
124
|
+
it('does not call get when creation succeeds', async () => {
|
|
125
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
126
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
127
|
+
|
|
128
|
+
const args: TArgs = {};
|
|
129
|
+
|
|
130
|
+
const getFn = jest.fn(
|
|
131
|
+
async (
|
|
132
|
+
_vc: ViewerContext,
|
|
133
|
+
_args: TArgs,
|
|
134
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
135
|
+
) => {
|
|
136
|
+
return null;
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const createFn = jest.fn(
|
|
141
|
+
async (vc: ViewerContext, _args: TArgs, queryContext?: EntityTransactionalQueryContext) => {
|
|
142
|
+
return await SimpleTestEntity.creator(vc, queryContext).createAsync();
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (inTransaction) {
|
|
147
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
148
|
+
'postgres',
|
|
149
|
+
async (queryContext) => {
|
|
150
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
151
|
+
viewerContext,
|
|
152
|
+
SimpleTestEntity,
|
|
153
|
+
getFn,
|
|
154
|
+
args,
|
|
155
|
+
createFn,
|
|
156
|
+
args,
|
|
157
|
+
queryContext,
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
163
|
+
viewerContext,
|
|
164
|
+
SimpleTestEntity,
|
|
165
|
+
getFn,
|
|
166
|
+
args,
|
|
167
|
+
createFn,
|
|
168
|
+
args,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expect(getFn).toHaveBeenCalledTimes(0);
|
|
173
|
+
expect(createFn).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('calls get when database adapter throws EntityDatabaseAdapterUniqueConstraintError', async () => {
|
|
177
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
178
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
179
|
+
|
|
180
|
+
const entity = await SimpleTestEntity.creator(viewerContext).createAsync();
|
|
181
|
+
|
|
182
|
+
const args: TArgs = {};
|
|
183
|
+
|
|
184
|
+
const getFn = jest.fn(
|
|
185
|
+
async (
|
|
186
|
+
_vc: ViewerContext,
|
|
187
|
+
_args: TArgs,
|
|
188
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
189
|
+
) => {
|
|
190
|
+
return entity;
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const createFn = jest.fn(
|
|
195
|
+
async (
|
|
196
|
+
_vc: ViewerContext,
|
|
197
|
+
_args: TArgs,
|
|
198
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
199
|
+
) => {
|
|
200
|
+
throw new EntityDatabaseAdapterUniqueConstraintError('wat');
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (inTransaction) {
|
|
205
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
206
|
+
'postgres',
|
|
207
|
+
async (queryContext) => {
|
|
208
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
209
|
+
viewerContext,
|
|
210
|
+
SimpleTestEntity,
|
|
211
|
+
getFn,
|
|
212
|
+
args,
|
|
213
|
+
createFn,
|
|
214
|
+
args,
|
|
215
|
+
queryContext,
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
await createWithUniqueConstraintRecoveryAsync(
|
|
221
|
+
viewerContext,
|
|
222
|
+
SimpleTestEntity,
|
|
223
|
+
getFn,
|
|
224
|
+
args,
|
|
225
|
+
createFn,
|
|
226
|
+
args,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
expect(getFn).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(createFn).toHaveBeenCalledTimes(1);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('throws an EntityNotFoundError when database adapter throws EntityDatabaseAdapterUniqueConstraintError and getFn returns null', async () => {
|
|
235
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
236
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
237
|
+
|
|
238
|
+
const args: TArgs = {};
|
|
239
|
+
|
|
240
|
+
const getFn = jest.fn(
|
|
241
|
+
async (
|
|
242
|
+
_vc: ViewerContext,
|
|
243
|
+
_args: TArgs,
|
|
244
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
245
|
+
) => {
|
|
246
|
+
return null;
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const createFn = jest.fn(
|
|
251
|
+
async (
|
|
252
|
+
_vc: ViewerContext,
|
|
253
|
+
_args: TArgs,
|
|
254
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
255
|
+
) => {
|
|
256
|
+
throw new EntityDatabaseAdapterUniqueConstraintError('wat');
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (inTransaction) {
|
|
261
|
+
await expect(
|
|
262
|
+
viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
263
|
+
'postgres',
|
|
264
|
+
async (queryContext) => {
|
|
265
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
266
|
+
viewerContext,
|
|
267
|
+
SimpleTestEntity,
|
|
268
|
+
getFn,
|
|
269
|
+
args,
|
|
270
|
+
createFn,
|
|
271
|
+
args,
|
|
272
|
+
queryContext,
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
).rejects.toThrow(EntityNotFoundError);
|
|
277
|
+
} else {
|
|
278
|
+
await expect(
|
|
279
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
280
|
+
viewerContext,
|
|
281
|
+
SimpleTestEntity,
|
|
282
|
+
getFn,
|
|
283
|
+
args,
|
|
284
|
+
createFn,
|
|
285
|
+
args,
|
|
286
|
+
),
|
|
287
|
+
).rejects.toThrow(EntityNotFoundError);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
expect(getFn).toHaveBeenCalledTimes(1);
|
|
291
|
+
expect(createFn).toHaveBeenCalledTimes(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('rethrows whatever error is thrown from database adapter if not EntityDatabaseAdapterUniqueConstraintError', async () => {
|
|
295
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
296
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
297
|
+
|
|
298
|
+
const args: TArgs = {};
|
|
299
|
+
|
|
300
|
+
const getFn = jest.fn(
|
|
301
|
+
async (
|
|
302
|
+
_vc: ViewerContext,
|
|
303
|
+
_args: TArgs,
|
|
304
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
305
|
+
) => {
|
|
306
|
+
return null;
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const createFn = jest.fn(
|
|
311
|
+
async (
|
|
312
|
+
_vc: ViewerContext,
|
|
313
|
+
_args: TArgs,
|
|
314
|
+
_queryContext?: EntityTransactionalQueryContext,
|
|
315
|
+
) => {
|
|
316
|
+
throw new Error('wat');
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (inTransaction) {
|
|
321
|
+
await expect(
|
|
322
|
+
viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
323
|
+
'postgres',
|
|
324
|
+
async (queryContext) => {
|
|
325
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
326
|
+
viewerContext,
|
|
327
|
+
SimpleTestEntity,
|
|
328
|
+
getFn,
|
|
329
|
+
args,
|
|
330
|
+
createFn,
|
|
331
|
+
args,
|
|
332
|
+
queryContext,
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
),
|
|
336
|
+
).rejects.toThrow('wat');
|
|
337
|
+
} else {
|
|
338
|
+
await expect(
|
|
339
|
+
createWithUniqueConstraintRecoveryAsync(
|
|
340
|
+
viewerContext,
|
|
341
|
+
SimpleTestEntity,
|
|
342
|
+
getFn,
|
|
343
|
+
args,
|
|
344
|
+
createFn,
|
|
345
|
+
args,
|
|
346
|
+
),
|
|
347
|
+
).rejects.toThrow('wat');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
expect(getFn).toHaveBeenCalledTimes(0);
|
|
351
|
+
expect(createFn).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|