@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.
Files changed (80) hide show
  1. package/build/AuthorizationResultBasedEntityLoader.js +5 -1
  2. package/build/AuthorizationResultBasedEntityLoader.js.map +1 -1
  3. package/build/AuthorizationResultBasedEntityMutator.d.ts +87 -2
  4. package/build/AuthorizationResultBasedEntityMutator.js +122 -8
  5. package/build/AuthorizationResultBasedEntityMutator.js.map +1 -1
  6. package/build/EntityLoaderUtils.d.ts +15 -3
  7. package/build/EntityLoaderUtils.js +25 -10
  8. package/build/EntityLoaderUtils.js.map +1 -1
  9. package/build/EntityQueryContext.d.ts +53 -7
  10. package/build/EntityQueryContext.js +65 -10
  11. package/build/EntityQueryContext.js.map +1 -1
  12. package/build/EntityQueryContextProvider.d.ts +5 -1
  13. package/build/EntityQueryContextProvider.js +11 -4
  14. package/build/EntityQueryContextProvider.js.map +1 -1
  15. package/build/IEntityGenericCacher.d.ts +2 -2
  16. package/build/errors/EntityNotFoundError.d.ts +8 -1
  17. package/build/errors/EntityNotFoundError.js +7 -2
  18. package/build/errors/EntityNotFoundError.js.map +1 -1
  19. package/build/index.d.ts +1 -0
  20. package/build/index.js +1 -0
  21. package/build/index.js.map +1 -1
  22. package/build/internal/CompositeFieldHolder.d.ts +13 -0
  23. package/build/internal/CompositeFieldHolder.js +7 -0
  24. package/build/internal/CompositeFieldHolder.js.map +1 -1
  25. package/build/internal/CompositeFieldValueMap.d.ts +3 -0
  26. package/build/internal/CompositeFieldValueMap.js +3 -0
  27. package/build/internal/CompositeFieldValueMap.js.map +1 -1
  28. package/build/internal/EntityDataManager.d.ts +22 -3
  29. package/build/internal/EntityDataManager.js +99 -11
  30. package/build/internal/EntityDataManager.js.map +1 -1
  31. package/build/internal/EntityFieldTransformationUtils.d.ts +20 -0
  32. package/build/internal/EntityFieldTransformationUtils.js +15 -0
  33. package/build/internal/EntityFieldTransformationUtils.js.map +1 -1
  34. package/build/internal/EntityLoadInterfaces.d.ts +8 -0
  35. package/build/internal/EntityLoadInterfaces.js +2 -0
  36. package/build/internal/EntityLoadInterfaces.js.map +1 -1
  37. package/build/internal/EntityTableDataCoordinator.d.ts +2 -0
  38. package/build/internal/EntityTableDataCoordinator.js +2 -0
  39. package/build/internal/EntityTableDataCoordinator.js.map +1 -1
  40. package/build/internal/ReadThroughEntityCache.d.ts +8 -0
  41. package/build/internal/ReadThroughEntityCache.js +5 -0
  42. package/build/internal/ReadThroughEntityCache.js.map +1 -1
  43. package/build/internal/SingleFieldHolder.d.ts +7 -0
  44. package/build/internal/SingleFieldHolder.js +7 -0
  45. package/build/internal/SingleFieldHolder.js.map +1 -1
  46. package/build/metrics/EntityMetricsUtils.d.ts +4 -3
  47. package/build/metrics/EntityMetricsUtils.js +6 -3
  48. package/build/metrics/EntityMetricsUtils.js.map +1 -1
  49. package/build/metrics/IEntityMetricsAdapter.d.ts +21 -0
  50. package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
  51. package/build/tsconfig.build.tsbuildinfo +1 -1
  52. package/build/utils/EntityCreationUtils.d.ts +14 -0
  53. package/build/utils/EntityCreationUtils.js +57 -0
  54. package/build/utils/EntityCreationUtils.js.map +1 -0
  55. package/package.json +13 -13
  56. package/src/AuthorizationResultBasedEntityLoader.ts +7 -1
  57. package/src/AuthorizationResultBasedEntityMutator.ts +133 -15
  58. package/src/EntityLoaderUtils.ts +43 -12
  59. package/src/EntityQueryContext.ts +68 -13
  60. package/src/EntityQueryContextProvider.ts +20 -3
  61. package/src/IEntityGenericCacher.ts +2 -2
  62. package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +98 -0
  63. package/src/__tests__/EntityQueryContext-test.ts +141 -26
  64. package/src/errors/EntityNotFoundError.ts +51 -4
  65. package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +26 -0
  66. package/src/index.ts +1 -0
  67. package/src/internal/CompositeFieldHolder.ts +15 -0
  68. package/src/internal/CompositeFieldValueMap.ts +3 -0
  69. package/src/internal/EntityDataManager.ts +170 -10
  70. package/src/internal/EntityFieldTransformationUtils.ts +20 -0
  71. package/src/internal/EntityLoadInterfaces.ts +8 -0
  72. package/src/internal/EntityTableDataCoordinator.ts +2 -0
  73. package/src/internal/ReadThroughEntityCache.ts +8 -0
  74. package/src/internal/SingleFieldHolder.ts +7 -0
  75. package/src/internal/__tests__/EntityDataManager-test.ts +708 -186
  76. package/src/metrics/EntityMetricsUtils.ts +7 -0
  77. package/src/metrics/IEntityMetricsAdapter.ts +27 -0
  78. package/src/utils/EntityCreationUtils.ts +143 -0
  79. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -1
  80. 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
+ });