@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
@@ -13,6 +13,10 @@ export interface EntityMetricsLoadEvent {
13
13
  * EntityMetricsLoadType for this load.
14
14
  */
15
15
  type: EntityMetricsLoadType;
16
+ /**
17
+ * Whether this load is within a transaction.
18
+ */
19
+ isInTransaction: boolean;
16
20
  /**
17
21
  * Class name of the Entity being loaded.
18
22
  */
@@ -36,6 +40,10 @@ export interface EntityMetricsMutationEvent {
36
40
  * EntityMetricsMutationType for this mutation.
37
41
  */
38
42
  type: EntityMetricsMutationType;
43
+ /**
44
+ * Whether this mutation is within a transaction.
45
+ */
46
+ isInTransaction: boolean;
39
47
  /**
40
48
  * Class name of the Entity being mutated.
41
49
  */
@@ -68,6 +76,10 @@ export interface IncrementLoadCountEvent {
68
76
  * Type of this event.
69
77
  */
70
78
  type: IncrementLoadCountEventType;
79
+ /**
80
+ * Whether this load is within a transaction.
81
+ */
82
+ isInTransaction: boolean;
71
83
  /**
72
84
  * Load method type for this event.
73
85
  */
@@ -93,8 +105,17 @@ export interface EntityMetricsAuthorizationEvent {
93
105
  * Class name of the Entity being authorized.
94
106
  */
95
107
  entityClassName: string;
108
+ /**
109
+ * The action being authorized.
110
+ */
96
111
  action: EntityAuthorizationAction;
112
+ /**
113
+ * The result of the authorization.
114
+ */
97
115
  evaluationResult: EntityMetricsAuthorizationResult;
116
+ /**
117
+ * The evaluation mode of the privacy policy.
118
+ */
98
119
  privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode;
99
120
  }
100
121
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"IEntityMetricsAdapter.js","sourceRoot":"","sources":["../../src/metrics/IEntityMetricsAdapter.ts"],"names":[],"mappings":";;;AAMA,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC/B,2EAAS,CAAA;IACT,qHAA8B,CAAA;IAC9B,mFAAa,CAAA;AACf,CAAC,EAJW,qBAAqB,qCAArB,qBAAqB,QAIhC;AA2BD,IAAY,yBAIX;AAJD,WAAY,yBAAyB;IACnC,6EAAM,CAAA;IACN,6EAAM,CAAA;IACN,6EAAM,CAAA;AACR,CAAC,EAJW,yBAAyB,yCAAzB,yBAAyB,QAIpC;AAmBD,IAAY,2BAgBX;AAhBD,WAAY,2BAA2B;IACrC;;;OAGG;IACH,yFAAU,CAAA;IAEV;;OAEG;IACH,+EAAK,CAAA;IAEL;;OAEG;IACH,qFAAQ,CAAA;AACV,CAAC,EAhBW,2BAA2B,2CAA3B,2BAA2B,QAgBtC;AA2BD,IAAY,gCAGX;AAHD,WAAY,gCAAgC;IAC1C,uFAAI,CAAA;IACJ,yFAAK,CAAA;AACP,CAAC,EAHW,gCAAgC,gDAAhC,gCAAgC,QAG3C"}
1
+ {"version":3,"file":"IEntityMetricsAdapter.js","sourceRoot":"","sources":["../../src/metrics/IEntityMetricsAdapter.ts"],"names":[],"mappings":";;;AAMA,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC/B,2EAAS,CAAA;IACT,qHAA8B,CAAA;IAC9B,mFAAa,CAAA;AACf,CAAC,EAJW,qBAAqB,qCAArB,qBAAqB,QAIhC;AAgCD,IAAY,yBAIX;AAJD,WAAY,yBAAyB;IACnC,6EAAM,CAAA;IACN,6EAAM,CAAA;IACN,6EAAM,CAAA;AACR,CAAC,EAJW,yBAAyB,yCAAzB,yBAAyB,QAIpC;AAwBD,IAAY,2BAgBX;AAhBD,WAAY,2BAA2B;IACrC;;;OAGG;IACH,yFAAU,CAAA;IAEV;;OAEG;IACH,+EAAK,CAAA;IAEL;;OAEG;IACH,qFAAQ,CAAA;AACV,CAAC,EAhBW,2BAA2B,2CAA3B,2BAA2B,QAgBtC;AAgCD,IAAY,gCAGX;AAHD,WAAY,gCAAgC;IAC1C,uFAAI,CAAA;IACJ,yFAAK,CAAA;AACP,CAAC,EAHW,gCAAgC,gDAAhC,gCAAgC,QAG3C"}
@@ -1 +1 @@
1
- {"root":["../src/authorizationresultbasedentityassociationloader.ts","../src/authorizationresultbasedentityloader.ts","../src/authorizationresultbasedentitymutator.ts","../src/composedentitycacheadapter.ts","../src/composedsecondaryentitycache.ts","../src/enforcingentityassociationloader.ts","../src/enforcingentitycreator.ts","../src/enforcingentitydeleter.ts","../src/enforcingentityloader.ts","../src/enforcingentityupdater.ts","../src/entity.ts","../src/entityassociationloader.ts","../src/entitycompanion.ts","../src/entitycompanionprovider.ts","../src/entityconfiguration.ts","../src/entitycreator.ts","../src/entitydatabaseadapter.ts","../src/entitydeleter.ts","../src/entityfielddefinition.ts","../src/entityfields.ts","../src/entityloader.ts","../src/entityloaderfactory.ts","../src/entityloaderutils.ts","../src/entitymutationinfo.ts","../src/entitymutationtriggerconfiguration.ts","../src/entitymutationvalidator.ts","../src/entitymutatorfactory.ts","../src/entityprivacypolicy.ts","../src/entityquerycontext.ts","../src/entityquerycontextprovider.ts","../src/entitysecondarycacheloader.ts","../src/entityupdater.ts","../src/genericentitycacheadapter.ts","../src/genericsecondaryentitycache.ts","../src/ientitycacheadapter.ts","../src/ientitycacheadapterprovider.ts","../src/ientitydatabaseadapterprovider.ts","../src/ientitygenericcacher.ts","../src/readonlyentity.ts","../src/viewercontext.ts","../src/viewerscopedentitycompanion.ts","../src/viewerscopedentitycompanionprovider.ts","../src/viewerscopedentityloaderfactory.ts","../src/viewerscopedentitymutatorfactory.ts","../src/entityutils.ts","../src/index.ts","../src/errors/entitycacheadaptererror.ts","../src/errors/entitydatabaseadaptererror.ts","../src/errors/entityerror.ts","../src/errors/entityinvalidfieldvalueerror.ts","../src/errors/entitynotauthorizederror.ts","../src/errors/entitynotfounderror.ts","../src/internal/compositefieldholder.ts","../src/internal/compositefieldvaluemap.ts","../src/internal/entitydatamanager.ts","../src/internal/entityfieldtransformationutils.ts","../src/internal/entityloadinterfaces.ts","../src/internal/entitytabledatacoordinator.ts","../src/internal/readthroughentitycache.ts","../src/internal/singlefieldholder.ts","../src/metrics/entitymetricsutils.ts","../src/metrics/ientitymetricsadapter.ts","../src/metrics/noopentitymetricsadapter.ts","../src/rules/alwaysallowprivacypolicyrule.ts","../src/rules/alwaysdenyprivacypolicyrule.ts","../src/rules/alwaysskipprivacypolicyrule.ts","../src/rules/privacypolicyrule.ts","../src/utils/entityprivacyutils.ts","../src/utils/mergeentitymutationtriggerconfigurations.ts","../src/utils/collections/serializablekeymap.ts","../src/utils/collections/maps.ts","../src/utils/collections/sets.ts"],"version":"5.8.3"}
1
+ {"root":["../src/authorizationresultbasedentityassociationloader.ts","../src/authorizationresultbasedentityloader.ts","../src/authorizationresultbasedentitymutator.ts","../src/composedentitycacheadapter.ts","../src/composedsecondaryentitycache.ts","../src/enforcingentityassociationloader.ts","../src/enforcingentitycreator.ts","../src/enforcingentitydeleter.ts","../src/enforcingentityloader.ts","../src/enforcingentityupdater.ts","../src/entity.ts","../src/entityassociationloader.ts","../src/entitycompanion.ts","../src/entitycompanionprovider.ts","../src/entityconfiguration.ts","../src/entitycreator.ts","../src/entitydatabaseadapter.ts","../src/entitydeleter.ts","../src/entityfielddefinition.ts","../src/entityfields.ts","../src/entityloader.ts","../src/entityloaderfactory.ts","../src/entityloaderutils.ts","../src/entitymutationinfo.ts","../src/entitymutationtriggerconfiguration.ts","../src/entitymutationvalidator.ts","../src/entitymutatorfactory.ts","../src/entityprivacypolicy.ts","../src/entityquerycontext.ts","../src/entityquerycontextprovider.ts","../src/entitysecondarycacheloader.ts","../src/entityupdater.ts","../src/genericentitycacheadapter.ts","../src/genericsecondaryentitycache.ts","../src/ientitycacheadapter.ts","../src/ientitycacheadapterprovider.ts","../src/ientitydatabaseadapterprovider.ts","../src/ientitygenericcacher.ts","../src/readonlyentity.ts","../src/viewercontext.ts","../src/viewerscopedentitycompanion.ts","../src/viewerscopedentitycompanionprovider.ts","../src/viewerscopedentityloaderfactory.ts","../src/viewerscopedentitymutatorfactory.ts","../src/entityutils.ts","../src/index.ts","../src/errors/entitycacheadaptererror.ts","../src/errors/entitydatabaseadaptererror.ts","../src/errors/entityerror.ts","../src/errors/entityinvalidfieldvalueerror.ts","../src/errors/entitynotauthorizederror.ts","../src/errors/entitynotfounderror.ts","../src/internal/compositefieldholder.ts","../src/internal/compositefieldvaluemap.ts","../src/internal/entitydatamanager.ts","../src/internal/entityfieldtransformationutils.ts","../src/internal/entityloadinterfaces.ts","../src/internal/entitytabledatacoordinator.ts","../src/internal/readthroughentitycache.ts","../src/internal/singlefieldholder.ts","../src/metrics/entitymetricsutils.ts","../src/metrics/ientitymetricsadapter.ts","../src/metrics/noopentitymetricsadapter.ts","../src/rules/alwaysallowprivacypolicyrule.ts","../src/rules/alwaysdenyprivacypolicyrule.ts","../src/rules/alwaysskipprivacypolicyrule.ts","../src/rules/privacypolicyrule.ts","../src/utils/entitycreationutils.ts","../src/utils/entityprivacyutils.ts","../src/utils/mergeentitymutationtriggerconfigurations.ts","../src/utils/collections/serializablekeymap.ts","../src/utils/collections/maps.ts","../src/utils/collections/sets.ts"],"version":"5.8.3"}
@@ -0,0 +1,14 @@
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
+ /**
7
+ * Create an entity if it doesn't exist, or get the existing entity if it does.
8
+ */
9
+ export declare function createOrGetExistingAsync<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, TGetArgs, TCreateArgs, TSelectedFields extends keyof TFields = keyof TFields>(viewerContext: TViewerContext, entityClass: IEntityClass<TFields, TIDField, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>, getAsync: (viewerContext: TViewerContext, getArgs: TGetArgs, queryContext?: EntityTransactionalQueryContext) => Promise<TEntity | null>, getArgs: TGetArgs, createAsync: (viewerContext: TViewerContext, createArgs: TCreateArgs, queryContext?: EntityTransactionalQueryContext) => Promise<TEntity>, createArgs: TCreateArgs, queryContext?: EntityTransactionalQueryContext): Promise<TEntity>;
10
+ /**
11
+ * Account for concurrent requests that may try to create the same entity.
12
+ * Return the existing entity if we get a Unique Constraint error.
13
+ */
14
+ export declare function createWithUniqueConstraintRecoveryAsync<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, TGetArgs, TCreateArgs, TSelectedFields extends keyof TFields = keyof TFields>(viewerContext: TViewerContext, entityClass: IEntityClass<TFields, TIDField, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields>, getAsync: (viewerContext: TViewerContext, getArgs: TGetArgs, queryContext?: EntityTransactionalQueryContext) => Promise<TEntity | null>, getArgs: TGetArgs, createAsync: (viewerContext: TViewerContext, createArgs: TCreateArgs, queryContext?: EntityTransactionalQueryContext) => Promise<TEntity>, createArgs: TCreateArgs, queryContext?: EntityTransactionalQueryContext): Promise<TEntity>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createOrGetExistingAsync = createOrGetExistingAsync;
7
+ exports.createWithUniqueConstraintRecoveryAsync = createWithUniqueConstraintRecoveryAsync;
8
+ const EntityDatabaseAdapterError_1 = require("../errors/EntityDatabaseAdapterError");
9
+ const EntityNotFoundError_1 = __importDefault(require("../errors/EntityNotFoundError"));
10
+ /**
11
+ * Create an entity if it doesn't exist, or get the existing entity if it does.
12
+ */
13
+ async function createOrGetExistingAsync(viewerContext, entityClass, getAsync, getArgs, createAsync, createArgs, queryContext) {
14
+ if (!queryContext) {
15
+ const maybeEntity = await getAsync(viewerContext, getArgs);
16
+ if (maybeEntity) {
17
+ return maybeEntity;
18
+ }
19
+ }
20
+ else {
21
+ // This is done in a nested transaction since entity may negatively cache load results per-transaction (when configured).
22
+ // Without it, it would
23
+ // 1. load the entity in the current query context, negatively cache it
24
+ // 2. then try to create it in the nested transaction, which may fail due to a unique constraint error
25
+ // 3. then try to load the entity again in the current query context, which would return null due to negative cache
26
+ const maybeEntity = await queryContext.runInNestedTransactionAsync((nestedQueryContext) => getAsync(viewerContext, getArgs, nestedQueryContext));
27
+ if (maybeEntity) {
28
+ return maybeEntity;
29
+ }
30
+ }
31
+ return await createWithUniqueConstraintRecoveryAsync(viewerContext, entityClass, getAsync, getArgs, createAsync, createArgs, queryContext);
32
+ }
33
+ /**
34
+ * Account for concurrent requests that may try to create the same entity.
35
+ * Return the existing entity if we get a Unique Constraint error.
36
+ */
37
+ async function createWithUniqueConstraintRecoveryAsync(viewerContext, entityClass, getAsync, getArgs, createAsync, createArgs, queryContext) {
38
+ try {
39
+ if (!queryContext) {
40
+ return await createAsync(viewerContext, createArgs);
41
+ }
42
+ return await queryContext.runInNestedTransactionAsync((nestedQueryContext) => createAsync(viewerContext, createArgs, nestedQueryContext));
43
+ }
44
+ catch (e) {
45
+ if (e instanceof EntityDatabaseAdapterError_1.EntityDatabaseAdapterUniqueConstraintError) {
46
+ const entity = await getAsync(viewerContext, getArgs, queryContext);
47
+ if (!entity) {
48
+ throw new EntityNotFoundError_1.default(`Expected entity to exist after unique constraint error: ${entityClass.name}`);
49
+ }
50
+ return entity;
51
+ }
52
+ else {
53
+ throw e;
54
+ }
55
+ }
56
+ }
57
+ //# sourceMappingURL=EntityCreationUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityCreationUtils.js","sourceRoot":"","sources":["../../src/utils/EntityCreationUtils.ts"],"names":[],"mappings":";;;;;AAWA,4DAkEC;AAMD,0FA2DC;AAzID,qFAAkG;AAClG,wFAAgE;AAEhE;;GAEG;AACI,KAAK,UAAU,wBAAwB,CAgB5C,aAA6B,EAC7B,WAOC,EACD,QAI4B,EAC5B,OAAiB,EACjB,WAIqB,EACrB,UAAuB,EACvB,YAA8C;IAE9C,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;QAC3D,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,WAAW,CAAC;QACrB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,yHAAyH;QACzH,uBAAuB;QACvB,uEAAuE;QACvE,sGAAsG;QACtG,mHAAmH;QACnH,MAAM,WAAW,GAAG,MAAM,YAAY,CAAC,2BAA2B,CAAC,CAAC,kBAAkB,EAAE,EAAE,CACxF,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,kBAAkB,CAAC,CACrD,CAAC;QACF,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,WAAW,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,uCAAuC,CAClD,aAAa,EACb,WAAW,EACX,QAAQ,EACR,OAAO,EACP,WAAW,EACX,UAAU,EACV,YAAY,CACb,CAAC;AACJ,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,uCAAuC,CAgB3D,aAA6B,EAC7B,WAOC,EACD,QAI4B,EAC5B,OAAiB,EACjB,WAIqB,EACrB,UAAuB,EACvB,YAA8C;IAE9C,IAAI,CAAC;QACH,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,MAAM,WAAW,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,MAAM,YAAY,CAAC,2BAA2B,CAAC,CAAC,kBAAkB,EAAE,EAAE,CAC3E,WAAW,CAAC,aAAa,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAC3D,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,uEAA0C,EAAE,CAAC;YAC5D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;YACpE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,6BAAmB,CAC3B,2DAA2D,WAAW,CAAC,IAAI,EAAE,CAC9E,CAAC;YACJ,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/entity",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "A privacy-first data model",
5
5
  "files": [
6
6
  "build",
@@ -29,11 +29,9 @@
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
31
  "@expo/results": "^1.0.0",
32
- "dataloader": "^2.0.0",
32
+ "dataloader": "^2.2.3",
33
33
  "es6-error": "^4.1.1",
34
- "invariant": "^2.2.4",
35
- "uuid": "^8.3.0",
36
- "uuidv7": "^1.0.0"
34
+ "invariant": "^2.2.4"
37
35
  },
38
36
  "devDependencies": {
39
37
  "@types/invariant": "^2.2.37",
@@ -41,17 +39,19 @@
41
39
  "@types/lodash": "^4.17.16",
42
40
  "@types/node": "^20.14.1",
43
41
  "@types/uuid": "^8.3.0",
44
- "ctix": "^2.7.0",
45
- "eslint": "^8.57.1",
46
- "eslint-config-universe": "^14.0.0",
47
- "eslint-plugin-tsdoc": "^0.3.0",
42
+ "ctix": "^2.7.1",
43
+ "eslint": "^9.26.0",
44
+ "eslint-config-universe": "^15.0.3",
45
+ "eslint-plugin-tsdoc": "^0.4.0",
48
46
  "jest": "^29.7.0",
49
47
  "lodash": "^4.17.21",
50
- "prettier": "^3.3.3",
48
+ "prettier": "^3.5.3",
51
49
  "prettier-plugin-organize-imports": "^4.1.0",
52
- "ts-jest": "^29.3.1",
50
+ "ts-jest": "^29.3.2",
53
51
  "ts-mockito": "^2.6.1",
54
- "typescript": "^5.8.3"
52
+ "typescript": "^5.8.3",
53
+ "uuid": "^8.3.0",
54
+ "uuidv7": "^1.0.0"
55
55
  },
56
- "gitHead": "618383027dc76b9250f22d163321ec25e2af0115"
56
+ "gitHead": "fe2d246f87adc98a13cc7c1ae57f3628769560c9"
57
57
  }
@@ -237,7 +237,13 @@ export default class AuthorizationResultBasedEntityLoader<
237
237
  const entityResult = entityResultsForId[0];
238
238
  return (
239
239
  entityResult ??
240
- result(new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id))
240
+ result(
241
+ new EntityNotFoundError({
242
+ entityClass: this.entityClass,
243
+ fieldName: this.entityConfiguration.idField,
244
+ fieldValue: id,
245
+ }),
246
+ )
241
247
  );
242
248
  });
243
249
  }
@@ -27,7 +27,93 @@ import { timeAndLogMutationEventAsync } from './metrics/EntityMetricsUtils';
27
27
  import IEntityMetricsAdapter, { EntityMetricsMutationType } from './metrics/IEntityMetricsAdapter';
28
28
  import { mapMapAsync } from './utils/collections/maps';
29
29
 
30
- abstract class AuthorizationResultBasedBaseMutator<
30
+ /**
31
+ * Base class for entity mutators. Mutators are builder-like class instances that are
32
+ * responsible for creating, updating, and deleting entities, and for calling out to
33
+ * the loader at appropriate times to invalidate the cache(s). The loader is responsible
34
+ * for deciding which cache entries to invalidate for the entity being mutated.
35
+ *
36
+ * ## Notes on invalidation
37
+ *
38
+ * The primary goal of invalidation is to ensure that at any point in time, a load
39
+ * for an entity through a cache or layers of caches will return the most up-to-date
40
+ * value for that entity according to the source of truth stored in the database, and thus
41
+ * the read-through cache must be kept consistent (only current values are stored, others are invalidated).
42
+ *
43
+ * This is done by invalidating the cache for the entity being mutated at the end of the transaction
44
+ * in which the mutation is performed. This ensures that the cache is invalidated as close as possible
45
+ * to when the source-of-truth is updated in the database, as to reduce the likelihood of
46
+ * collisions with loads done at the same time on other machines.
47
+ *
48
+ * <blockquote>
49
+ * The easiest way to demonstrate this reasoning is via some counter-examples.
50
+ * For sake of demonstration, let's say we did invalidation immediately after the mutation instead of
51
+ * at the end of the transaction.
52
+ *
53
+ * Example 1:
54
+ * - t=0. A transaction is started on machine A and within this transaction a new entity is created.
55
+ * The cache for the entity is invalidated.
56
+ * - t=1. Machine B tries to load the same entity outside of a transaction. It does not yet exist
57
+ * so it negatively caches the entity.
58
+ * - t=2. Machine A commits the transaction.
59
+ * - t=3. Machine C tries to load the same entity outside of a transaction. It is negatively cached
60
+ * so it returns null, even though it exists in the database.
61
+ *
62
+ * One can see that it's strictly better to invalidate the transaction at t=2 as it would remove the
63
+ * negative cache entry for the entity, thus leaving the cache consistent with the database.
64
+ *
65
+ * Example 2:
66
+ * - t=0. Entity A is created and read into the cache (everthing is consistent at this point in time).
67
+ * - t=1. Machine A starts a transaction, reads entity A, updates it, and invalidates the cache.
68
+ * - t=2. Machine B reads entity A outside of a transaction. Since the transaction from the step above
69
+ * has not yet been committed, the changes within that transaction are not yet visible. It stores
70
+ * the entity in the cache.
71
+ * - t=3. Machine A commits the transaction.
72
+ * - t=4. Machine C reads entity A outside of a transaction. It returns the entity from the cache which
73
+ * is now inconsistent with the database.
74
+ *
75
+ * Again, one can see that it's strictly better to invalidate the transaction at t=3 as it would remove the
76
+ * stale cache entry for the entity, thus leaving the cache consistent with the database.
77
+ *
78
+ * For deletions, one can imagine a similar series of events occurring.
79
+ * </blockquote>
80
+ *
81
+ * #### Invalidation as it pertains to transactions and nested transactions
82
+ *
83
+ * Invalidation becomes slightly more complex when nested transactions are considered. The general
84
+ * guiding principle here is that over-invalidation is strictly better than under-invalidation
85
+ * as far as consistency goes. This is because the database is the source of truth.
86
+ *
87
+ * For the visible-to-the-outside-world caches (cache adapters), the invalidations are done at the
88
+ * end of the outermost transaction (as discussed above), plus at the end of each nested transaction.
89
+ * While only the outermost transaction is strictly necessary for these cache adapter invalidations,
90
+ * the mental model of doing it at the end of each transaction, nested or otherwise, is easier to reason about.
91
+ *
92
+ * For the dataloader caches (per-transaction local caches), the invalidation is done multiple times
93
+ * (over-invalidation) to better ensure that the caches are always consistent with the database as read within
94
+ * the transaction or nested transaction.
95
+ * 1. Immediately after the mutation is performed (but before the transaction or nested transaction is committed).
96
+ * 2. At the end of the transaction (or nested transaction) itself.
97
+ * 3. At the end of the outermost transaction (if this is a nested transaction) and all of that transactions's nested transactions recursively.
98
+ *
99
+ * This over-invalidation is done because transaction isolation semantics are not consistent across all
100
+ * databases (some databases don't even have true nested transactions at all), meaning that whether
101
+ * a change made in a nested transaction is visible to the parent transaction(s) is not necessarily known.
102
+ * This means that the only way to ensure that the dataloader caches are consistent
103
+ * with the database is to invalidate them often, thus delegating consistency to the database. Invalidation
104
+ * of local caches is synchronous and immediate, so the performance impact of over-invalidation is negligible.
105
+ *
106
+ * #### Invalidation pitfalls
107
+ *
108
+ * One may have noticed that the above invalidation strategy still isn't perfect. Cache invalidation is hard.
109
+ * There still exists a very short moment in time between when invalidation occurs and when the transaction is committed,
110
+ * so dirty cache writes are still possible, especially in systems reading an object frequently and writing to the same object.
111
+ * For now, the entity framework does not attempt to provide a further solution to this problem since it is likely
112
+ * solutions will be case-specific. Some fun reads on the topic:
113
+ * - https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/
114
+ * - https://hazelcast.com/blog/a-hitchhikers-guide-to-caching-patterns/
115
+ */
116
+ export abstract class AuthorizationResultBasedBaseMutator<
31
117
  TFields extends Record<string, any>,
32
118
  TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
33
119
  TViewerContext extends ViewerContext,
@@ -224,6 +310,7 @@ export class AuthorizationResultBasedCreateMutator<
224
310
  this.metricsAdapter,
225
311
  EntityMetricsMutationType.CREATE,
226
312
  this.entityClass.name,
313
+ this.queryContext,
227
314
  )(this.createInTransactionAsync());
228
315
  }
229
316
 
@@ -282,9 +369,14 @@ export class AuthorizationResultBasedCreateMutator<
282
369
 
283
370
  const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity);
284
371
 
285
- queryContext.appendPostCommitInvalidationCallback(
286
- entityLoader.utils.invalidateFieldsAsync.bind(entityLoader, insertResult),
287
- );
372
+ // Invalidate all caches for the new entity so that any previously-negatively-cached loads
373
+ // are removed from the caches.
374
+ queryContext.appendPostCommitInvalidationCallback(async () => {
375
+ entityLoader.utils.invalidateFieldsForTransaction(queryContext, insertResult);
376
+ await entityLoader.utils.invalidateFieldsAsync(insertResult);
377
+ });
378
+
379
+ entityLoader.utils.invalidateFieldsForTransaction(queryContext, insertResult);
288
380
 
289
381
  const unauthorizedEntityAfterInsert = entityLoader.utils.constructEntity(insertResult);
290
382
  const newEntity = await enforceAsyncResult(
@@ -423,6 +515,7 @@ export class AuthorizationResultBasedUpdateMutator<
423
515
  this.metricsAdapter,
424
516
  EntityMetricsMutationType.UPDATE,
425
517
  this.entityClass.name,
518
+ this.queryContext,
426
519
  )(this.updateInTransactionAsync(false, null));
427
520
  }
428
521
 
@@ -499,15 +592,31 @@ export class AuthorizationResultBasedUpdateMutator<
499
592
  );
500
593
  }
501
594
 
502
- queryContext.appendPostCommitInvalidationCallback(
503
- entityLoader.utils.invalidateFieldsAsync.bind(
504
- entityLoader,
595
+ // Invalidate all caches for the entity being updated so that any previously-cached loads
596
+ // are consistent. This means:
597
+ // - any query that returned this entity (pre-update) in the past should no longer have that entity in cache for that query.
598
+ // - any query that will return this entity (post-update) that would not have returned the entity in the past should not
599
+ // be negatively cached for the entity.
600
+ // To do this we simply invalidate all of the entity's caches for both the previous version of the entity and the upcoming
601
+ // version of the entity.
602
+
603
+ queryContext.appendPostCommitInvalidationCallback(async () => {
604
+ entityLoader.utils.invalidateFieldsForTransaction(
605
+ queryContext,
505
606
  this.originalEntity.getAllDatabaseFields(),
506
- ),
507
- );
508
- queryContext.appendPostCommitInvalidationCallback(
509
- entityLoader.utils.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity),
607
+ );
608
+ entityLoader.utils.invalidateFieldsForTransaction(queryContext, this.fieldsForEntity);
609
+ await Promise.all([
610
+ entityLoader.utils.invalidateFieldsAsync(this.originalEntity.getAllDatabaseFields()),
611
+ entityLoader.utils.invalidateFieldsAsync(this.fieldsForEntity),
612
+ ]);
613
+ });
614
+
615
+ entityLoader.utils.invalidateFieldsForTransaction(
616
+ queryContext,
617
+ this.originalEntity.getAllDatabaseFields(),
510
618
  );
619
+ entityLoader.utils.invalidateFieldsForTransaction(queryContext, this.fieldsForEntity);
511
620
 
512
621
  const updatedEntity = await enforceAsyncResult(
513
622
  entityLoader.loadByIDAsync(entityAboutToBeUpdated.getID()),
@@ -639,6 +748,7 @@ export class AuthorizationResultBasedDeleteMutator<
639
748
  this.metricsAdapter,
640
749
  EntityMetricsMutationType.DELETE,
641
750
  this.entityClass.name,
751
+ this.queryContext,
642
752
  )(this.deleteInTransactionAsync(new Set(), false, null));
643
753
  }
644
754
 
@@ -708,11 +818,19 @@ export class AuthorizationResultBasedDeleteMutator<
708
818
  previousValue: null,
709
819
  cascadingDeleteCause,
710
820
  });
711
- queryContext.appendPostCommitInvalidationCallback(
712
- entityLoader.utils.invalidateFieldsAsync.bind(
713
- entityLoader,
821
+
822
+ // Invalidate all caches for the entity so that any previously-cached loads
823
+ // are removed from the caches.
824
+ queryContext.appendPostCommitInvalidationCallback(async () => {
825
+ entityLoader.utils.invalidateFieldsForTransaction(
826
+ queryContext,
714
827
  this.entity.getAllDatabaseFields(),
715
- ),
828
+ );
829
+ await entityLoader.utils.invalidateFieldsAsync(this.entity.getAllDatabaseFields());
830
+ });
831
+ entityLoader.utils.invalidateFieldsForTransaction(
832
+ queryContext,
833
+ this.entity.getAllDatabaseFields(),
716
834
  );
717
835
 
718
836
  await this.executeMutationTriggersAsync(
@@ -4,11 +4,12 @@ import nullthrows from 'nullthrows';
4
4
  import { IEntityClass } from './Entity';
5
5
  import EntityConfiguration from './EntityConfiguration';
6
6
  import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
7
- import { EntityQueryContext } from './EntityQueryContext';
7
+ import { EntityQueryContext, EntityTransactionalQueryContext } from './EntityQueryContext';
8
8
  import ReadonlyEntity from './ReadonlyEntity';
9
9
  import ViewerContext from './ViewerContext';
10
10
  import { pick } from './entityUtils';
11
11
  import EntityDataManager from './internal/EntityDataManager';
12
+ import { LoadPair } from './internal/EntityLoadInterfaces';
12
13
  import { SingleFieldHolder, SingleFieldValueHolder } from './internal/SingleFieldHolder';
13
14
  import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
14
15
  import { mapMapAsync } from './utils/collections/maps';
@@ -56,11 +57,9 @@ export default class EntityLoaderUtils<
56
57
  protected readonly metricsAdapter: IEntityMetricsAdapter,
57
58
  ) {}
58
59
 
59
- /**
60
- * Invalidate all caches for an entity's fields. Exposed primarily for internal use by EntityMutator.
61
- * @param objectFields - entity data object to be invalidated
62
- */
63
- async invalidateFieldsAsync(objectFields: Readonly<TFields>): Promise<void> {
60
+ private getKeyValuePairsFromObjectFields(
61
+ objectFields: Readonly<TFields>,
62
+ ): readonly LoadPair<TFields, TIDField, any, any, any>[] {
64
63
  const keys = Object.keys(objectFields) as (keyof TFields)[];
65
64
  const singleFieldKeyValues = keys
66
65
  .map((fieldName: keyof TFields) => {
@@ -85,22 +84,54 @@ export default class EntityLoaderUtils<
85
84
  : null;
86
85
  })
87
86
  .filter((kv) => kv !== null);
87
+ return [...singleFieldKeyValues, ...compositeFieldKeyValues];
88
+ }
89
+
90
+ /**
91
+ * Invalidate all caches and local dataloaders for an entity's fields. Exposed primarily for internal use by EntityMutator.
92
+ * @param objectFields - entity data object to be invalidated
93
+ */
94
+ public async invalidateFieldsAsync(objectFields: Readonly<TFields>): Promise<void> {
95
+ await this.dataManager.invalidateKeyValuePairsAsync(
96
+ this.getKeyValuePairsFromObjectFields(objectFields),
97
+ );
98
+ }
88
99
 
89
- await this.dataManager.invalidateKeyValuePairsAsync([
90
- ...singleFieldKeyValues,
91
- ...compositeFieldKeyValues,
92
- ]);
100
+ /**
101
+ * Invalidate all local dataloaders specific to a transaction for an entity's fields. Exposed primarily for internal use by EntityMutator.
102
+ * @param objectFields - entity data object to be invalidated
103
+ */
104
+ public invalidateFieldsForTransaction(
105
+ queryContext: EntityTransactionalQueryContext,
106
+ objectFields: Readonly<TFields>,
107
+ ): void {
108
+ this.dataManager.invalidateKeyValuePairsForTransaction(
109
+ queryContext,
110
+ this.getKeyValuePairsFromObjectFields(objectFields),
111
+ );
93
112
  }
94
113
 
95
114
  /**
96
- * Invalidate all caches for an entity. One potential use case would be to keep the entity
115
+ * Invalidate all caches and local dataloaders for an entity. One potential use case would be to keep the entity
97
116
  * framework in sync with changes made to data outside of the framework.
98
117
  * @param entity - entity to be invalidated
99
118
  */
100
- async invalidateEntityAsync(entity: TEntity): Promise<void> {
119
+ public async invalidateEntityAsync(entity: TEntity): Promise<void> {
101
120
  await this.invalidateFieldsAsync(entity.getAllDatabaseFields());
102
121
  }
103
122
 
123
+ /**
124
+ * Invalidate all local dataloaders specific to a transaction for an entity. One potential use case would be to keep the entity
125
+ * framework in sync with changes made to data outside of the framework.
126
+ * @param entity - entity to be invalidated
127
+ */
128
+ public invalidateEntityForTransaction(
129
+ queryContext: EntityTransactionalQueryContext,
130
+ entity: TEntity,
131
+ ): void {
132
+ this.invalidateFieldsForTransaction(queryContext, entity.getAllDatabaseFields());
133
+ }
134
+
104
135
  /**
105
136
  * Construct an entity from a fields object (applying field selection if applicable),
106
137
  * checking that the ID field is specified.