@expo/entity 0.55.0 → 0.57.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 (163) hide show
  1. package/build/src/AuthorizationResultBasedEntityAssociationLoader.d.ts +1 -1
  2. package/build/src/AuthorizationResultBasedEntityAssociationLoader.js.map +1 -1
  3. package/build/src/AuthorizationResultBasedEntityLoader.d.ts +18 -24
  4. package/build/src/AuthorizationResultBasedEntityLoader.js +37 -56
  5. package/build/src/AuthorizationResultBasedEntityLoader.js.map +1 -1
  6. package/build/src/AuthorizationResultBasedEntityMutator.js +26 -19
  7. package/build/src/AuthorizationResultBasedEntityMutator.js.map +1 -1
  8. package/build/src/EnforcingEntityCreator.d.ts +1 -1
  9. package/build/src/EnforcingEntityCreator.js +1 -1
  10. package/build/src/EnforcingEntityLoader.d.ts +1 -58
  11. package/build/src/EnforcingEntityLoader.js +0 -65
  12. package/build/src/EnforcingEntityLoader.js.map +1 -1
  13. package/build/src/Entity.d.ts +6 -0
  14. package/build/src/Entity.js +6 -0
  15. package/build/src/Entity.js.map +1 -1
  16. package/build/src/EntityCompanion.d.ts +2 -2
  17. package/build/src/EntityCompanion.js.map +1 -1
  18. package/build/src/EntityCompanionProvider.d.ts +1 -1
  19. package/build/src/EntityCompanionProvider.js +4 -4
  20. package/build/src/EntityConfiguration.d.ts +1 -1
  21. package/build/src/EntityConfiguration.js +1 -2
  22. package/build/src/EntityConfiguration.js.map +1 -1
  23. package/build/src/{EntityLoaderUtils.d.ts → EntityConstructionUtils.d.ts} +15 -29
  24. package/build/src/EntityConstructionUtils.js +118 -0
  25. package/build/src/EntityConstructionUtils.js.map +1 -0
  26. package/build/src/EntityDatabaseAdapter.d.ts +10 -108
  27. package/build/src/EntityDatabaseAdapter.js +14 -76
  28. package/build/src/EntityDatabaseAdapter.js.map +1 -1
  29. package/build/src/EntityFieldDefinition.d.ts +1 -1
  30. package/build/src/EntityInvalidationUtils.d.ts +41 -0
  31. package/build/src/EntityInvalidationUtils.js +71 -0
  32. package/build/src/EntityInvalidationUtils.js.map +1 -0
  33. package/build/src/EntityLoader.d.ts +0 -6
  34. package/build/src/EntityLoader.js +0 -7
  35. package/build/src/EntityLoader.js.map +1 -1
  36. package/build/src/EntityLoaderFactory.d.ts +4 -0
  37. package/build/src/EntityLoaderFactory.js +10 -3
  38. package/build/src/EntityLoaderFactory.js.map +1 -1
  39. package/build/src/EntityPrivacyPolicy.d.ts +27 -0
  40. package/build/src/EntityPrivacyPolicy.js +22 -1
  41. package/build/src/EntityPrivacyPolicy.js.map +1 -1
  42. package/build/src/EntitySecondaryCacheLoader.d.ts +14 -3
  43. package/build/src/EntitySecondaryCacheLoader.js +21 -4
  44. package/build/src/EntitySecondaryCacheLoader.js.map +1 -1
  45. package/build/src/ReadonlyEntity.d.ts +4 -5
  46. package/build/src/ReadonlyEntity.js +7 -8
  47. package/build/src/ReadonlyEntity.js.map +1 -1
  48. package/build/src/ViewerContext.d.ts +6 -6
  49. package/build/src/ViewerContext.js +8 -8
  50. package/build/src/ViewerScopedEntityCompanion.d.ts +1 -1
  51. package/build/src/ViewerScopedEntityCompanion.js.map +1 -1
  52. package/build/src/ViewerScopedEntityLoaderFactory.d.ts +4 -0
  53. package/build/src/ViewerScopedEntityLoaderFactory.js +6 -0
  54. package/build/src/ViewerScopedEntityLoaderFactory.js.map +1 -1
  55. package/build/src/errors/EntityDatabaseAdapterError.d.ts +4 -0
  56. package/build/src/errors/EntityDatabaseAdapterError.js +13 -1
  57. package/build/src/errors/EntityDatabaseAdapterError.js.map +1 -1
  58. package/build/src/errors/EntityError.d.ts +2 -1
  59. package/build/src/errors/EntityError.js +1 -0
  60. package/build/src/errors/EntityError.js.map +1 -1
  61. package/build/src/index.d.ts +2 -1
  62. package/build/src/index.js +2 -1
  63. package/build/src/index.js.map +1 -1
  64. package/build/src/internal/EntityDataManager.d.ts +8 -16
  65. package/build/src/internal/EntityDataManager.js +8 -18
  66. package/build/src/internal/EntityDataManager.js.map +1 -1
  67. package/build/src/internal/EntityFieldTransformationUtils.js.map +1 -1
  68. package/build/src/internal/EntityLoadInterfaces.d.ts +2 -0
  69. package/build/src/internal/EntityLoadInterfaces.js +2 -0
  70. package/build/src/internal/EntityLoadInterfaces.js.map +1 -1
  71. package/build/src/internal/EntityTableDataCoordinator.d.ts +2 -0
  72. package/build/src/internal/EntityTableDataCoordinator.js +4 -0
  73. package/build/src/internal/EntityTableDataCoordinator.js.map +1 -1
  74. package/build/src/metrics/EntityMetricsUtils.d.ts +1 -0
  75. package/build/src/metrics/EntityMetricsUtils.js +15 -1
  76. package/build/src/metrics/EntityMetricsUtils.js.map +1 -1
  77. package/build/src/metrics/IEntityMetricsAdapter.d.ts +4 -1
  78. package/build/src/metrics/IEntityMetricsAdapter.js +3 -0
  79. package/build/src/metrics/IEntityMetricsAdapter.js.map +1 -1
  80. package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.d.ts +2 -2
  81. package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.d.ts +2 -2
  82. package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.d.ts +2 -2
  83. package/build/src/rules/PrivacyPolicyRule.d.ts +2 -2
  84. package/build/src/utils/EntityPrivacyUtils.js +11 -20
  85. package/build/src/utils/EntityPrivacyUtils.js.map +1 -1
  86. package/build/src/utils/collections/maps.d.ts +2 -2
  87. package/build/src/utils/collections/maps.js +2 -2
  88. package/package.json +4 -4
  89. package/src/AuthorizationResultBasedEntityAssociationLoader.ts +4 -7
  90. package/src/AuthorizationResultBasedEntityLoader.ts +58 -88
  91. package/src/AuthorizationResultBasedEntityMutator.ts +35 -20
  92. package/src/EnforcingEntityCreator.ts +1 -1
  93. package/src/EnforcingEntityLoader.ts +1 -95
  94. package/src/Entity.ts +6 -0
  95. package/src/EntityCompanion.ts +2 -2
  96. package/src/EntityCompanionProvider.ts +4 -4
  97. package/src/EntityConfiguration.ts +8 -5
  98. package/src/EntityConstructionUtils.ts +168 -0
  99. package/src/EntityDatabaseAdapter.ts +32 -222
  100. package/src/EntityFieldDefinition.ts +1 -1
  101. package/src/{EntityLoaderUtils.ts → EntityInvalidationUtils.ts} +5 -96
  102. package/src/EntityLoader.ts +0 -16
  103. package/src/EntityLoaderFactory.ts +50 -10
  104. package/src/EntityPrivacyPolicy.ts +44 -1
  105. package/src/EntitySecondaryCacheLoader.ts +54 -3
  106. package/src/ReadonlyEntity.ts +9 -11
  107. package/src/ViewerContext.ts +10 -10
  108. package/src/ViewerScopedEntityCompanion.ts +1 -1
  109. package/src/ViewerScopedEntityLoaderFactory.ts +37 -0
  110. package/src/__tests__/AuthorizationResultBasedEntityLoader-constructor-test.ts +3 -5
  111. package/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +34 -419
  112. package/src/__tests__/ComposedCacheAdapter-test.ts +3 -3
  113. package/src/__tests__/EnforcingEntityLoader-test.ts +2 -134
  114. package/src/__tests__/EntityCompanion-test.ts +18 -0
  115. package/src/__tests__/EntityConfiguration-test.ts +4 -4
  116. package/src/__tests__/EntityDatabaseAdapter-test.ts +33 -68
  117. package/src/__tests__/EntityEdges-test.ts +10 -10
  118. package/src/__tests__/EntityLoader-test.ts +6 -4
  119. package/src/__tests__/EntityMutator-test.ts +27 -15
  120. package/src/__tests__/EntityPrivacyPolicy-test.ts +102 -0
  121. package/src/__tests__/EntityQueryContext-test.ts +11 -11
  122. package/src/__tests__/EntitySecondaryCacheLoader-test.ts +10 -5
  123. package/src/__tests__/EntitySelfReferentialEdges-test.ts +6 -6
  124. package/src/__tests__/GenericEntityCacheAdapter-test.ts +18 -15
  125. package/src/__tests__/GenericSecondaryEntityCache-test.ts +27 -5
  126. package/src/__tests__/ReadonlyEntity-test.ts +6 -4
  127. package/src/__tests__/ViewerContext-test.ts +4 -4
  128. package/src/__tests__/ViewerScopedEntityCompanion-test.ts +1 -0
  129. package/src/__tests__/cases/TwoEntitySameTableDisjointRows-test.ts +0 -17
  130. package/src/errors/EntityDatabaseAdapterError.ts +14 -0
  131. package/src/errors/EntityError.ts +1 -0
  132. package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +9 -0
  133. package/src/errors/__tests__/EntityError-test.ts +13 -5
  134. package/src/index.ts +2 -1
  135. package/src/internal/EntityDataManager.ts +19 -54
  136. package/src/internal/EntityFieldTransformationUtils.ts +5 -5
  137. package/src/internal/EntityLoadInterfaces.ts +2 -0
  138. package/src/internal/EntityTableDataCoordinator.ts +2 -2
  139. package/src/internal/__tests__/CompositeFieldHolder-test.ts +8 -2
  140. package/src/internal/__tests__/EntityDataManager-test.ts +71 -202
  141. package/src/internal/__tests__/ReadThroughEntityCache-test.ts +39 -24
  142. package/src/metrics/EntityMetricsUtils.ts +23 -0
  143. package/src/metrics/IEntityMetricsAdapter.ts +3 -0
  144. package/src/metrics/__tests__/EntityMetricsUtils-test.ts +120 -0
  145. package/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts +2 -2
  146. package/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts +2 -2
  147. package/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts +2 -2
  148. package/src/rules/PrivacyPolicyRule.ts +2 -2
  149. package/src/rules/__tests__/AllowIfAllSubRulesAllowPrivacyPolicyRule-test.ts +4 -4
  150. package/src/rules/__tests__/AllowIfAnySubRuleAllowsPrivacyPolicyRule-test.ts +4 -4
  151. package/src/rules/__tests__/AllowIfInParentCascadeDeletionPrivacyPolicyRule-test.ts +11 -1
  152. package/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +2 -2
  153. package/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +2 -2
  154. package/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +2 -2
  155. package/src/rules/__tests__/EvaluateIfEntityFieldPredicatePrivacyPolicyRule-test.ts +3 -3
  156. package/src/utils/EntityPrivacyUtils.ts +18 -29
  157. package/src/utils/__testfixtures__/PrivacyPolicyRuleTestUtils.ts +2 -2
  158. package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +13 -101
  159. package/src/utils/__tests__/EntityCreationUtils-test.ts +6 -6
  160. package/src/utils/__tests__/EntityPrivacyUtils-test.ts +2 -2
  161. package/src/utils/collections/maps.ts +2 -2
  162. package/build/src/EntityLoaderUtils.js +0 -147
  163. package/build/src/EntityLoaderUtils.js.map +0 -1
@@ -1,10 +1,5 @@
1
1
  import { AuthorizationResultBasedEntityLoader } from './AuthorizationResultBasedEntityLoader';
2
2
  import { EntityCompositeField, EntityCompositeFieldValue } from './EntityConfiguration';
3
- import {
4
- FieldEqualityCondition,
5
- QuerySelectionModifiers,
6
- QuerySelectionModifiersWithOrderByRaw,
7
- } from './EntityDatabaseAdapter';
8
3
  import { EntityPrivacyPolicy } from './EntityPrivacyPolicy';
9
4
  import { ReadonlyEntity } from './ReadonlyEntity';
10
5
  import { ViewerContext } from './ViewerContext';
@@ -29,7 +24,7 @@ export class EnforcingEntityLoader<
29
24
  TEntity,
30
25
  TSelectedFields
31
26
  >,
32
- TSelectedFields extends keyof TFields,
27
+ TSelectedFields extends keyof TFields = keyof TFields,
33
28
  > {
34
29
  constructor(
35
30
  private readonly entityLoader: AuthorizationResultBasedEntityLoader<
@@ -219,93 +214,4 @@ export class EnforcingEntityLoader<
219
214
  const entityResults = await this.entityLoader.loadManyByIDsNullableAsync(ids);
220
215
  return mapMap(entityResults, (result) => result?.enforceValue() ?? null);
221
216
  }
222
-
223
- /**
224
- * Loads the first entity matching the selection constructed from the conjunction of specified
225
- * operands, or null if no matching entity exists. Entities loaded using this method are not
226
- * batched or cached.
227
- *
228
- * This is a convenience method for {@link loadManyByFieldEqualityConjunctionAsync}. However, the
229
- * `orderBy` option must be specified to define what "first" means. If ordering doesn't matter,
230
- * explicitly pass in an empty array.
231
- *
232
- * @param fieldEqualityOperands - list of field equality selection operand specifications
233
- * @param querySelectionModifiers - orderBy and optional offset for the query
234
- * @returns the first entity that matches the query or null if no entity matches the query
235
- * @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
236
- */
237
- async loadFirstByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
238
- fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
239
- querySelectionModifiers: Omit<QuerySelectionModifiers<TFields>, 'limit'> &
240
- Required<Pick<QuerySelectionModifiers<TFields>, 'orderBy'>>,
241
- ): Promise<TEntity | null> {
242
- const entityResult = await this.entityLoader.loadFirstByFieldEqualityConjunctionAsync(
243
- fieldEqualityOperands,
244
- querySelectionModifiers,
245
- );
246
- return entityResult ? entityResult.enforceValue() : null;
247
- }
248
-
249
- /**
250
- * Loads many entities matching the selection constructed from the conjunction of specified operands.
251
- * Entities loaded using this method are not batched or cached.
252
- *
253
- * @example
254
- * fieldEqualityOperands:
255
- * `[{fieldName: 'hello', fieldValue: 1}, {fieldName: 'world', fieldValues: [2, 3]}]`
256
- * Entities returned with a SQL EntityDatabaseAdapter:
257
- * `WHERE hello = 1 AND world = ANY({2, 3})`
258
- *
259
- * @param fieldEqualityOperands - list of field equality selection operand specifications
260
- * @param querySelectionModifiers - limit, offset, and orderBy for the query
261
- * @returns array of entities that match the query
262
- * @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
263
- */
264
- async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
265
- fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
266
- querySelectionModifiers: QuerySelectionModifiers<TFields> = {},
267
- ): Promise<readonly TEntity[]> {
268
- const entityResults = await this.entityLoader.loadManyByFieldEqualityConjunctionAsync(
269
- fieldEqualityOperands,
270
- querySelectionModifiers,
271
- );
272
- return entityResults.map((result) => result.enforceValue());
273
- }
274
-
275
- /**
276
- * Loads many entities matching the raw WHERE clause. Corresponds to the knex `whereRaw` argument format.
277
- *
278
- * @remarks
279
- * Important notes:
280
- * - Fields in clause are database column names instead of transformed entity field names.
281
- * - Entities loaded using this method are not batched or cached.
282
- * - Not all database adapters implement the ability to execute this method of fetching entities.
283
- *
284
- * @example
285
- * rawWhereClause: `id = ?`
286
- * bindings: `[1]`
287
- * Entites returned `WHERE id = 1`
288
- *
289
- * http://knexjs.org/#Builder-whereRaw
290
- * http://knexjs.org/#Raw-Bindings
291
- *
292
- * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
293
- * @param bindings - array of positional bindings or object of named bindings
294
- * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
295
- * @returns array of entities that match the query
296
- * @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
297
- * @throws Error when rawWhereClause or bindings are invalid
298
- */
299
- async loadManyByRawWhereClauseAsync(
300
- rawWhereClause: string,
301
- bindings: any[] | object,
302
- querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw<TFields> = {},
303
- ): Promise<readonly TEntity[]> {
304
- const entityResults = await this.entityLoader.loadManyByRawWhereClauseAsync(
305
- rawWhereClause,
306
- bindings,
307
- querySelectionModifiers,
308
- );
309
- return entityResults.map((result) => result.enforceValue());
310
- }
311
217
  }
package/src/Entity.ts CHANGED
@@ -32,6 +32,12 @@ import { ViewerContext } from './ViewerContext';
32
32
  *
33
33
  * All concrete entity implementations should extend this class and provide their
34
34
  * own EntityCompanionDefinition.
35
+ *
36
+ * Generic type parameters:
37
+ * TFields - the shape of the underlying data for this entity, typically corresponding to a database table schema. The mapping from TFields to the actual database schema is defined in the EntityCompanionDefinition for this entity.
38
+ * TIDField - the key of the ID field in TFields, which must be non-nullable and is used to uniquely identify individual entities
39
+ * TViewerContext - the type of ViewerContext that can be used with this entity
40
+ * TSelectedFields - the keys of fields in TFields that belong to this entity; used when there are multiple entities backed by the same underlying table with different field subsets
35
41
  */
36
42
  export abstract class Entity<
37
43
  TFields extends Record<string, any>,
@@ -59,8 +59,8 @@ export class EntityCompanion<
59
59
  TPrivacyPolicy,
60
60
  TSelectedFields
61
61
  >,
62
- private readonly tableDataCoordinator: EntityTableDataCoordinator<TFields, TIDField>,
63
- private readonly metricsAdapter: IEntityMetricsAdapter,
62
+ public readonly tableDataCoordinator: EntityTableDataCoordinator<TFields, TIDField>,
63
+ public readonly metricsAdapter: IEntityMetricsAdapter,
64
64
  ) {
65
65
  this.privacyPolicy = new entityCompanionDefinition.privacyPolicyClass();
66
66
  this.entityLoaderFactory = new EntityLoaderFactory<
@@ -208,13 +208,13 @@ export class EntityCompanionProvider {
208
208
  });
209
209
  }
210
210
 
211
- getQueryContextProviderForDatabaseAdaptorFlavor(
211
+ getQueryContextProviderForDatabaseAdapterFlavor(
212
212
  databaseAdapterFlavor: DatabaseAdapterFlavor,
213
213
  ): EntityQueryContextProvider {
214
214
  const entityDatabaseAdapterFlavor = this.databaseAdapterFlavors.get(databaseAdapterFlavor);
215
215
  invariant(
216
216
  entityDatabaseAdapterFlavor,
217
- `No database adaptor configuration found for flavor: ${databaseAdapterFlavor}`,
217
+ `No database adapter configuration found for flavor: ${databaseAdapterFlavor}`,
218
218
  );
219
219
 
220
220
  return entityDatabaseAdapterFlavor.queryContextProvider;
@@ -233,7 +233,7 @@ export class EntityCompanionProvider {
233
233
  );
234
234
  invariant(
235
235
  entityDatabaseAdapterFlavor,
236
- `No database adaptor configuration found for flavor: ${entityConfiguration.databaseAdapterFlavor}`,
236
+ `No database adapter configuration found for flavor: ${entityConfiguration.databaseAdapterFlavor}`,
237
237
  );
238
238
 
239
239
  const entityCacheAdapterFlavor = this.cacheAdapterFlavors.get(
@@ -241,7 +241,7 @@ export class EntityCompanionProvider {
241
241
  );
242
242
  invariant(
243
243
  entityCacheAdapterFlavor,
244
- `No cache adaptor configuration found for flavor: ${entityConfiguration.cacheAdapterFlavor}`,
244
+ `No cache adapter configuration found for flavor: ${entityConfiguration.cacheAdapterFlavor}`,
245
245
  );
246
246
 
247
247
  return new EntityTableDataCoordinator(
@@ -67,7 +67,9 @@ export class CompositeFieldInfo<
67
67
  keyDefinition.compositeField.length === new Set(keyDefinition.compositeField).size,
68
68
  'Composite field must have unique sub-fields',
69
69
  );
70
- const compositeFieldHolder = new CompositeFieldHolder(keyDefinition.compositeField);
70
+ const compositeFieldHolder = new CompositeFieldHolder<TFields, TIDField>(
71
+ keyDefinition.compositeField,
72
+ );
71
73
  return [
72
74
  compositeFieldHolder.serialize(),
73
75
  { compositeFieldHolder, cache: keyDefinition.cache ?? false },
@@ -79,8 +81,9 @@ export class CompositeFieldInfo<
79
81
  public getCompositeFieldHolderForCompositeField(
80
82
  compositeField: EntityCompositeField<TFields>,
81
83
  ): CompositeFieldHolder<TFields, TIDField> | undefined {
82
- return this.compositeFieldInfoMap.get(new CompositeFieldHolder(compositeField).serialize())
83
- ?.compositeFieldHolder;
84
+ return this.compositeFieldInfoMap.get(
85
+ new CompositeFieldHolder<TFields, TIDField>(compositeField).serialize(),
86
+ )?.compositeFieldHolder;
84
87
  }
85
88
 
86
89
  public getAllCompositeFieldHolders(): readonly CompositeFieldHolder<TFields, TIDField>[] {
@@ -89,7 +92,7 @@ export class CompositeFieldInfo<
89
92
 
90
93
  public canCacheCompositeField(compositeField: EntityCompositeField<TFields>): boolean {
91
94
  const compositeFieldInfo = this.compositeFieldInfoMap.get(
92
- new CompositeFieldHolder(compositeField).serialize(),
95
+ new CompositeFieldHolder<TFields, TIDField>(compositeField).serialize(),
93
96
  );
94
97
  invariant(
95
98
  compositeFieldInfo,
@@ -107,7 +110,7 @@ export class EntityConfiguration<
107
110
  TFields extends Record<string, any>,
108
111
  TIDField extends keyof TFields,
109
112
  > {
110
- readonly idField: keyof TFields;
113
+ readonly idField: TIDField;
111
114
  readonly tableName: string;
112
115
  readonly cacheableKeys: ReadonlySet<keyof TFields>;
113
116
  readonly compositeFieldInfo: CompositeFieldInfo<TFields, TIDField>;
@@ -0,0 +1,168 @@
1
+ import { Result, asyncResult, result } from '@expo/results';
2
+ import invariant from 'invariant';
3
+ import nullthrows from 'nullthrows';
4
+
5
+ import { IEntityClass } from './Entity';
6
+ import { EntityConfiguration } from './EntityConfiguration';
7
+ import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
8
+ import { EntityQueryContext } from './EntityQueryContext';
9
+ import { ReadonlyEntity } from './ReadonlyEntity';
10
+ import { ViewerContext } from './ViewerContext';
11
+ import { pick } from './entityUtils';
12
+ import { EntityInvalidFieldValueError } from './errors/EntityInvalidFieldValueError';
13
+ import { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter';
14
+ import { mapMapAsync } from './utils/collections/maps';
15
+
16
+ /**
17
+ * Common entity loader utilities for entity construction and authorization.
18
+ * Methods are exposed publicly since in rare cases they may need to be called manually.
19
+ */
20
+ export class EntityConstructionUtils<
21
+ TFields extends Record<string, any>,
22
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
23
+ TViewerContext extends ViewerContext,
24
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
25
+ TPrivacyPolicy extends EntityPrivacyPolicy<
26
+ TFields,
27
+ TIDField,
28
+ TViewerContext,
29
+ TEntity,
30
+ TSelectedFields
31
+ >,
32
+ TSelectedFields extends keyof TFields,
33
+ > {
34
+ constructor(
35
+ private readonly viewerContext: TViewerContext,
36
+ private readonly queryContext: EntityQueryContext,
37
+ private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
38
+ TFields,
39
+ TIDField,
40
+ TViewerContext,
41
+ TEntity,
42
+ TSelectedFields
43
+ >,
44
+ private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
45
+ private readonly entityClass: IEntityClass<
46
+ TFields,
47
+ TIDField,
48
+ TViewerContext,
49
+ TEntity,
50
+ TPrivacyPolicy,
51
+ TSelectedFields
52
+ >,
53
+ private readonly entitySelectedFields: TSelectedFields[] | undefined,
54
+ private readonly privacyPolicy: TPrivacyPolicy,
55
+ protected readonly metricsAdapter: IEntityMetricsAdapter,
56
+ ) {}
57
+
58
+ /**
59
+ * Construct an entity from a fields object (applying field selection if applicable),
60
+ * checking that the ID field is specified.
61
+ *
62
+ * @param fieldsObject - fields object
63
+ */
64
+ public constructEntity(fieldsObject: TFields): TEntity {
65
+ const idField = this.entityConfiguration.idField;
66
+ const id = nullthrows(fieldsObject[idField], 'must provide ID to create an entity');
67
+ const entitySelectedFields =
68
+ this.entitySelectedFields ?? Array.from(this.entityConfiguration.schema.keys());
69
+ const selectedFields = pick(fieldsObject, entitySelectedFields);
70
+ return new this.entityClass({
71
+ viewerContext: this.viewerContext,
72
+ id: id as TFields[TIDField],
73
+ databaseFields: fieldsObject,
74
+ selectedFields,
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Construct and authorize entities from fields map, returning error results for entities that fail
80
+ * to construct or fail to authorize.
81
+ *
82
+ * @param map - map from an arbitrary key type to an array of entity field objects
83
+ */
84
+ public async constructAndAuthorizeEntitiesAsync<K>(
85
+ map: ReadonlyMap<K, readonly Readonly<TFields>[]>,
86
+ ): Promise<ReadonlyMap<K, readonly Result<TEntity>[]>> {
87
+ return await mapMapAsync(map, async (fieldObjects) => {
88
+ return await this.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Construct and authorize entities from field objects array, returning error results for entities that fail
94
+ * to construct or fail to authorize.
95
+ *
96
+ * @param fieldObjects - array of field objects
97
+ */
98
+ public async constructAndAuthorizeEntitiesArrayAsync(
99
+ fieldObjects: readonly Readonly<TFields>[],
100
+ ): Promise<readonly Result<TEntity>[]> {
101
+ const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
102
+ return await Promise.all(
103
+ uncheckedEntityResults.map((uncheckedEntityResult) =>
104
+ this.authorizeEntityResultAsync(uncheckedEntityResult),
105
+ ),
106
+ );
107
+ }
108
+
109
+ private async authorizeEntityResultAsync(
110
+ uncheckedEntityResult: Result<TEntity>,
111
+ ): Promise<Result<TEntity>> {
112
+ if (!uncheckedEntityResult.ok) {
113
+ return uncheckedEntityResult;
114
+ }
115
+ return await asyncResult(
116
+ this.privacyPolicy.authorizeReadAsync(
117
+ this.viewerContext,
118
+ this.queryContext,
119
+ this.privacyPolicyEvaluationContext,
120
+ uncheckedEntityResult.value,
121
+ this.metricsAdapter,
122
+ ),
123
+ );
124
+ }
125
+
126
+ public async constructAndAuthorizeEntityAsync(
127
+ fieldsObject: Readonly<TFields>,
128
+ ): Promise<Result<TEntity>> {
129
+ const uncheckedEntityResult = this.tryConstructEntity(fieldsObject);
130
+ return await this.authorizeEntityResultAsync(uncheckedEntityResult);
131
+ }
132
+
133
+ /**
134
+ * Validate that field values are valid according to the field's validation function.
135
+ *
136
+ * @param fieldName - field name to validate
137
+ * @param fieldValues - field values to validate
138
+ * @throws EntityInvalidFieldValueError when a field value is invalid
139
+ */
140
+ public validateFieldAndValues<N extends keyof Pick<TFields, TSelectedFields>>(
141
+ fieldName: N,
142
+ fieldValues: readonly TFields[N][],
143
+ ): void {
144
+ const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
145
+ invariant(fieldDefinition, `must have field definition for field = ${String(fieldName)}`);
146
+ for (const fieldValue of fieldValues) {
147
+ const isInputValid = fieldDefinition.validateInputValue(fieldValue);
148
+ if (!isInputValid) {
149
+ throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
150
+ }
151
+ }
152
+ }
153
+
154
+ private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
155
+ return fieldsObjects.map((fieldsObject) => this.tryConstructEntity(fieldsObject));
156
+ }
157
+
158
+ private tryConstructEntity(fieldsObject: TFields): Result<TEntity> {
159
+ try {
160
+ return result(this.constructEntity(fieldsObject));
161
+ } catch (e) {
162
+ if (!(e instanceof Error)) {
163
+ throw e;
164
+ }
165
+ return result(e);
166
+ }
167
+ }
168
+ }
@@ -17,127 +17,6 @@ import {
17
17
  } from './internal/EntityFieldTransformationUtils';
18
18
  import { IEntityLoadKey, IEntityLoadValue } from './internal/EntityLoadInterfaces';
19
19
 
20
- /**
21
- * Equality operand that is used for selecting entities with a field with a single value.
22
- */
23
- export interface SingleValueFieldEqualityCondition<
24
- TFields extends Record<string, any>,
25
- N extends keyof TFields = keyof TFields,
26
- > {
27
- fieldName: N;
28
- fieldValue: TFields[N];
29
- }
30
-
31
- /**
32
- * Equality operand that is used for selecting entities with a field matching one of multiple values.
33
- */
34
- export interface MultiValueFieldEqualityCondition<
35
- TFields extends Record<string, any>,
36
- N extends keyof TFields = keyof TFields,
37
- > {
38
- fieldName: N;
39
- fieldValues: readonly TFields[N][];
40
- }
41
-
42
- /**
43
- * A single equality operand for use in a selection clause.
44
- * See EntityLoader.loadManyByFieldEqualityConjunctionAsync documentation for examples.
45
- */
46
- export type FieldEqualityCondition<
47
- TFields extends Record<string, any>,
48
- N extends keyof TFields = keyof TFields,
49
- > = SingleValueFieldEqualityCondition<TFields, N> | MultiValueFieldEqualityCondition<TFields, N>;
50
-
51
- export function isSingleValueFieldEqualityCondition<
52
- TFields extends Record<string, any>,
53
- N extends keyof TFields = keyof TFields,
54
- >(
55
- condition: FieldEqualityCondition<TFields, N>,
56
- ): condition is SingleValueFieldEqualityCondition<TFields, N> {
57
- return (condition as SingleValueFieldEqualityCondition<TFields, N>).fieldValue !== undefined;
58
- }
59
-
60
- export interface TableFieldSingleValueEqualityCondition {
61
- tableField: string;
62
- tableValue: any;
63
- }
64
-
65
- export interface TableFieldMultiValueEqualityCondition {
66
- tableField: string;
67
- tableValues: readonly any[];
68
- }
69
-
70
- /**
71
- * Ordering options for `orderBy` clauses.
72
- */
73
- export enum OrderByOrdering {
74
- /**
75
- * Ascending order (lowest to highest).
76
- * Ascending order puts smaller values first, where "smaller" is defined in terms of the %3C operator.
77
- */
78
- ASCENDING = 'asc',
79
-
80
- /**
81
- * Descending order (highest to lowest).
82
- * Descending order puts larger values first, where "larger" is defined in terms of the %3E operator.
83
- */
84
- DESCENDING = 'desc',
85
- }
86
-
87
- /**
88
- * SQL modifiers that only affect the selection but not the projection.
89
- */
90
- export interface QuerySelectionModifiers<TFields extends Record<string, any>> {
91
- /**
92
- * Order the entities by specified columns and orders.
93
- */
94
- orderBy?: {
95
- /**
96
- * The field name to order by.
97
- */
98
- fieldName: keyof TFields;
99
-
100
- /**
101
- * The OrderByOrdering to order by.
102
- */
103
- order: OrderByOrdering;
104
- }[];
105
-
106
- /**
107
- * Skip the specified number of entities queried before returning.
108
- */
109
- offset?: number;
110
-
111
- /**
112
- * Limit the number of entities returned.
113
- */
114
- limit?: number;
115
- }
116
-
117
- export interface QuerySelectionModifiersWithOrderByRaw<
118
- TFields extends Record<string, any>,
119
- > extends QuerySelectionModifiers<TFields> {
120
- /**
121
- * Order the entities by a raw SQL `ORDER BY` clause.
122
- */
123
- orderByRaw?: string;
124
- }
125
-
126
- export interface TableQuerySelectionModifiers {
127
- orderBy:
128
- | {
129
- columnName: string;
130
- order: OrderByOrdering;
131
- }[]
132
- | undefined;
133
- offset: number | undefined;
134
- limit: number | undefined;
135
- }
136
-
137
- export interface TableQuerySelectionModifiersWithOrderByRaw extends TableQuerySelectionModifiers {
138
- orderByRaw: string | undefined;
139
- }
140
-
141
20
  /**
142
21
  * A database adapter is an interface by which entity objects can be
143
22
  * fetched, inserted, updated, and deleted from a database. This base class
@@ -148,9 +27,9 @@ export abstract class EntityDatabaseAdapter<
148
27
  TFields extends Record<string, any>,
149
28
  TIDField extends keyof TFields,
150
29
  > {
151
- private readonly fieldTransformerMap: FieldTransformerMap;
30
+ protected readonly fieldTransformerMap: FieldTransformerMap;
152
31
 
153
- constructor(private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>) {
32
+ constructor(protected readonly entityConfiguration: EntityConfiguration<TFields, TIDField>) {
154
33
  this.fieldTransformerMap = this.getFieldTransformerMap();
155
34
  }
156
35
 
@@ -222,91 +101,51 @@ export abstract class EntityDatabaseAdapter<
222
101
  ): Promise<object[]>;
223
102
 
224
103
  /**
225
- * Fetch many objects matching the conjunction of where clauses constructed from
226
- * specified field equality operands.
104
+ * Fetch one objects where key is equal to value, null if no matching object exists.
105
+ * Returned object is not guaranteed to be deterministic. Most concrete implementations will implement this
106
+ * with a "first" or "limit 1" query.
227
107
  *
228
108
  * @param queryContext - query context with which to perform the fetch
229
- * @param fieldEqualityOperands - list of field equality where clause operand specifications
230
- * @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
231
- * @returns array of objects matching the query
109
+ * @param key - load key being queried
110
+ * @param values - load value being queried
111
+ * @returns object that matches the query for the value
232
112
  */
233
- async fetchManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(
113
+ async fetchOneWhereAsync<
114
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
115
+ TSerializedLoadValue,
116
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
117
+ >(
234
118
  queryContext: EntityQueryContext,
235
- fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
236
- querySelectionModifiers: QuerySelectionModifiers<TFields>,
237
- ): Promise<readonly Readonly<TFields>[]> {
238
- const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
239
- const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
240
- for (const operand of fieldEqualityOperands) {
241
- if (isSingleValueFieldEqualityCondition(operand)) {
242
- tableFieldSingleValueOperands.push({
243
- tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
244
- tableValue: operand.fieldValue,
245
- });
246
- } else {
247
- tableFieldMultipleValueOperands.push({
248
- tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
249
- tableValues: operand.fieldValues,
250
- });
251
- }
252
- }
119
+ key: TLoadKey,
120
+ value: TLoadValue,
121
+ ): Promise<Readonly<TFields> | null> {
122
+ const keyDatabaseColumns = key.getDatabaseColumns(this.entityConfiguration);
123
+ const valueDatabaseValue = key.getDatabaseValues(value);
253
124
 
254
- const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
125
+ const result = await this.fetchOneWhereInternalAsync(
255
126
  queryContext.getQueryInterface(),
256
127
  this.entityConfiguration.tableName,
257
- tableFieldSingleValueOperands,
258
- tableFieldMultipleValueOperands,
259
- this.convertToTableQueryModifiers(querySelectionModifiers),
260
- );
261
-
262
- return results.map((result) =>
263
- transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
128
+ keyDatabaseColumns,
129
+ valueDatabaseValue,
264
130
  );
265
- }
266
131
 
267
- protected abstract fetchManyByFieldEqualityConjunctionInternalAsync(
268
- queryInterface: any,
269
- tableName: string,
270
- tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
271
- tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
272
- querySelectionModifiers: TableQuerySelectionModifiers,
273
- ): Promise<object[]>;
274
-
275
- /**
276
- * Fetch many objects matching the raw WHERE clause.
277
- *
278
- * @param queryContext - query context with which to perform the fetch
279
- * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
280
- * @param bindings - array of positional bindings or object of named bindings
281
- * @param querySelectionModifiers - limit, offset, and orderBy for the query
282
- * @returns array of objects matching the query
283
- */
284
- async fetchManyByRawWhereClauseAsync(
285
- queryContext: EntityQueryContext,
286
- rawWhereClause: string,
287
- bindings: any[] | object,
288
- querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw<TFields>,
289
- ): Promise<readonly Readonly<TFields>[]> {
290
- const results = await this.fetchManyByRawWhereClauseInternalAsync(
291
- queryContext.getQueryInterface(),
292
- this.entityConfiguration.tableName,
293
- rawWhereClause,
294
- bindings,
295
- this.convertToTableQueryModifiersWithOrderByRaw(querySelectionModifiers),
296
- );
132
+ if (!result) {
133
+ return null;
134
+ }
297
135
 
298
- return results.map((result) =>
299
- transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
136
+ return transformDatabaseObjectToFields(
137
+ this.entityConfiguration,
138
+ this.fieldTransformerMap,
139
+ result,
300
140
  );
301
141
  }
302
142
 
303
- protected abstract fetchManyByRawWhereClauseInternalAsync(
143
+ protected abstract fetchOneWhereInternalAsync(
304
144
  queryInterface: any,
305
145
  tableName: string,
306
- rawWhereClause: string,
307
- bindings: any[] | object,
308
- querySelectionModifiers: TableQuerySelectionModifiersWithOrderByRaw,
309
- ): Promise<object[]>;
146
+ tableColumns: readonly string[],
147
+ tableTuple: readonly any[],
148
+ ): Promise<object | null>;
310
149
 
311
150
  /**
312
151
  * Insert an object.
@@ -446,33 +285,4 @@ export abstract class EntityDatabaseAdapter<
446
285
  tableIdField: string,
447
286
  id: any,
448
287
  ): Promise<number>;
449
-
450
- private convertToTableQueryModifiersWithOrderByRaw(
451
- querySelectionModifiers: QuerySelectionModifiersWithOrderByRaw<TFields>,
452
- ): TableQuerySelectionModifiersWithOrderByRaw {
453
- return {
454
- ...this.convertToTableQueryModifiers(querySelectionModifiers),
455
- orderByRaw: querySelectionModifiers.orderByRaw,
456
- };
457
- }
458
-
459
- private convertToTableQueryModifiers(
460
- querySelectionModifiers: QuerySelectionModifiers<TFields>,
461
- ): TableQuerySelectionModifiers {
462
- const orderBy = querySelectionModifiers.orderBy;
463
- return {
464
- orderBy:
465
- orderBy !== undefined
466
- ? orderBy.map((orderBySpecification) => ({
467
- columnName: getDatabaseFieldForEntityField(
468
- this.entityConfiguration,
469
- orderBySpecification.fieldName,
470
- ),
471
- order: orderBySpecification.order,
472
- }))
473
- : undefined,
474
- offset: querySelectionModifiers.offset,
475
- limit: querySelectionModifiers.limit,
476
- };
477
- }
478
288
  }
@@ -103,7 +103,7 @@ export interface EntityAssociationDefinition<
103
103
  * Field by which to load the instance of associatedEntityClass. If not provided, the
104
104
  * associatedEntityClass instance is fetched by its ID.
105
105
  */
106
- associatedEntityLookupByField?: keyof TAssociatedFields;
106
+ associatedEntityLookupByField?: TAssociatedSelectedFields;
107
107
 
108
108
  /**
109
109
  * What action to perform on the entity at the other end of this edge when the entity on the source end of