Compare commits

..
Author SHA1 Message Date
William Schurman 39093c3498 feat: add entity write validation 2020-06-23 20:43:20 -07:00
65 changed files with 622 additions and 1548 deletions

No files matched your search

+1 -2
View File
@@ -9,8 +9,7 @@
],
"rules": {
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
"tsdoc/syntax": "warn",
"no-console": "warn"
"tsdoc/syntax": "warn"
},
"overrides": [
{
-2
View File
@@ -10,8 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v1
with:
node-version: 10.x
+4 -23
View File
@@ -1,25 +1,6 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.5.0 (2020-07-01)
### Bug Fixes
* support number ID field generation in StubDatabaseAdapter ([#43](https://github.com/expo/entity/issues/43)) ([60cc0fb](https://github.com/expo/entity/commit/60cc0fbda460c3ada1f5244a2780f90382fe3ebd))
* upgrade Apollo server for security advisory ([#38](https://github.com/expo/entity/issues/38)) ([d25c1e1](https://github.com/expo/entity/commit/d25c1e142d52385725593f4f155d1795697ab490))
### Features
* add two fan out methods to association loader ([#52](https://github.com/expo/entity/issues/52)) ([89cfb3d](https://github.com/expo/entity/commit/89cfb3d5a01d4da90f64acc52ae1b839ba348a35))
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
# 0.4.0 (2020-06-03)
# Changelog
## v0.4.0 (2020-06-03)
* All Packages
* Documentation Improvements
@@ -35,11 +16,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
* `@expo/entity-example`
* Initial Version
# 0.3.0 (2020-04-28)
## v0.3.0 (2020-04-28)
* `@expo/entity`
* Initial Version
* `@expo/entity-cache-adapter-redis`
* Initial Version
* `@expo/entity-database-adapter-knex`
* Initial Version
* Initial Version
+1 -1
View File
@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.5.0"
"version": "0.4.0"
}
@@ -1,11 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.5.0 (2020-07-01)
### Features
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.5.0",
"version": "0.4.0",
"description": "Redis cache adapter for @expo/entity",
"files": [
"build",
@@ -32,6 +32,6 @@
"ioredis": "^4.16.1"
},
"devDependencies": {
"@expo/entity": "^0.5.0"
"@expo/entity": "^0.4.0"
}
}
@@ -44,7 +44,7 @@ describe(RedisCacheAdapter, () => {
const cacheAdapter = vc1.entityCompanionProvider.getCompanionForEntity(
RedisTestEntity,
RedisTestEntity.getCompanionDefinition()
)['tableDataCoordinator']['cacheAdapter'];
)['cacheAdapter'];
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
// creating an entity should put it in cache (loader loads it after creation)
@@ -1,10 +1,4 @@
import {
CacheStatus,
UUIDField,
EntityConfiguration,
DatabaseAdapterFlavor,
CacheAdapterFlavor,
} from '@expo/entity';
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
import { Redis, Pipeline } from 'ioredis';
import { mock, when, instance, anything, verify } from 'ts-mockito';
@@ -20,8 +14,6 @@ const entityConfiguration = new EntityConfiguration<BlahFields>({
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
describe(RedisCacheAdapter, () => {
@@ -85,12 +85,12 @@ export const redisTestEntityConfiguration = new EntityConfiguration<RedisTestEnt
columnName: 'date_field',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
const redisTestEntityCompanionDefinition = new EntityCompanionDefinition({
const redisTestEntityCompanionDefinition = {
entityClass: RedisTestEntity,
entityConfiguration: redisTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: RedisTestEntityPrivacyPolicy,
});
};
@@ -1,11 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.5.0 (2020-07-01)
### Features
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.5.0",
"version": "0.4.0",
"description": "Knex database adapter for @expo/entity",
"files": [
"build",
@@ -32,6 +32,6 @@
"knex": "^0.20.11"
},
"devDependencies": {
"@expo/entity": "^0.5.0"
"@expo/entity": "^0.4.0"
}
}
@@ -104,12 +104,12 @@ export const invalidTestEntityConfiguration = new EntityConfiguration<InvalidTes
columnName: 'name',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
const invalidTestEntityCompanionDefinition = new EntityCompanionDefinition({
const invalidTestEntityCompanionDefinition = {
entityClass: InvalidTestEntity,
entityConfiguration: invalidTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: InvalidTestEntityPrivacyPolicy,
});
};
@@ -150,12 +150,12 @@ export const postgresTestEntityConfiguration = new EntityConfiguration<PostgresT
columnName: 'maybe_json_array_field',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
const postgresTestEntityCompanionDefinition = new EntityCompanionDefinition({
const postgresTestEntityCompanionDefinition = {
entityClass: PostgresTestEntity,
entityConfiguration: postgresTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: PostgresTestEntityPrivacyPolicy,
});
};
-16
View File
@@ -1,16 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.5.0 (2020-07-01)
### Bug Fixes
* upgrade Apollo server for security advisory ([#38](https://github.com/expo/entity/issues/38)) ([d25c1e1](https://github.com/expo/entity/commit/d25c1e142d52385725593f4f155d1795697ab490))
### Features
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-example",
"private": true,
"version": "0.5.0",
"version": "0.4.0",
"description": "An example integration of the @expo/entity framework",
"scripts": {
"tsc": "tsc",
@@ -22,7 +22,7 @@
"author": "Expo",
"license": "MIT",
"dependencies": {
"@expo/entity": "^0.5.0",
"@expo/entity": "^0.4.0",
"apollo-server-koa": "^2.14.2",
"graphql": "^15.0.0",
"koa": "^2.11.0",
@@ -38,7 +38,7 @@ export default class NoteEntity extends Entity<NoteFields, string, ExampleViewer
* of entity. In some languages, this would be representable as "abstract" static members
* of the class itself, but TypeScript disallows static generic and abstract methods.
*/
export const noteEntityCompanion = new EntityCompanionDefinition({
export const noteEntityCompanion = {
entityClass: NoteEntity,
entityConfiguration: new EntityConfiguration<NoteFields>({
idField: 'id',
@@ -57,8 +57,8 @@ export const noteEntityCompanion = new EntityCompanionDefinition({
columnName: 'body',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
}),
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: NotePrivacyPolicy,
});
};
-17
View File
@@ -1,17 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# 0.5.0 (2020-07-01)
### Bug Fixes
* support number ID field generation in StubDatabaseAdapter ([#43](https://github.com/expo/entity/issues/43)) ([60cc0fb](https://github.com/expo/entity/commit/60cc0fbda460c3ada1f5244a2780f90382fe3ebd))
### Features
* add two fan out methods to association loader ([#52](https://github.com/expo/entity/issues/52)) ([89cfb3d](https://github.com/expo/entity/commit/89cfb3d5a01d4da90f64acc52ae1b839ba348a35))
* support entity fields subset of db fields ([#49](https://github.com/expo/entity/issues/49)) ([4e40b2e](https://github.com/expo/entity/commit/4e40b2e521407e521d236978ec3b3b56db3990be))
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.5.0",
"version": "0.4.0",
"description": "A privacy-first data model",
"files": [
"build",
+3 -11
View File
@@ -13,15 +13,8 @@ export default class EnforcingEntityLoader<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly entityLoader: EntityLoader<
@@ -29,8 +22,7 @@ export default class EnforcingEntityLoader<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>
) {}
+25 -118
View File
@@ -28,9 +28,8 @@ import ViewerContext from './ViewerContext';
export default abstract class Entity<
TFields,
TID,
TViewerContext extends ViewerContext,
TSelectedFields extends keyof TFields = keyof TFields
> extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields> {
TViewerContext extends ViewerContext
> extends ReadonlyEntity<TFields, TID, TViewerContext> {
/**
* Vend mutator for creating a new entity in given query context.
* @param viewerContext - viewer context of creating user
@@ -42,30 +41,16 @@ export default abstract class Entity<
TMID,
TMViewerContext extends ViewerContext,
TMViewerContext2 extends TMViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
viewerContext: TMViewerContext2,
queryContext: EntityQueryContext = viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): CreateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
): CreateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
return viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getMutatorFactory()
@@ -82,31 +67,17 @@ export default abstract class Entity<
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): UpdateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
): UpdateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
return existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
@@ -123,24 +94,10 @@ export default abstract class Entity<
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
@@ -165,24 +122,10 @@ export default abstract class Entity<
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
@@ -218,24 +161,10 @@ export default abstract class Entity<
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
@@ -267,24 +196,10 @@ export default abstract class Entity<
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
existingEntity: TMEntity,
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
@@ -311,15 +226,8 @@ export interface IEntityClass<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
new (viewerContext: TViewerContext, obj: Readonly<TFields>): TEntity;
getCompanionDefinition(): EntityCompanionDefinition<
@@ -327,7 +235,6 @@ export interface IEntityClass<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>;
}
+46 -219
View File
@@ -10,8 +10,7 @@ export default class EntityAssociationLoader<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> {
constructor(private readonly entity: TEntity) {}
@@ -25,29 +24,21 @@ export default class EntityAssociationLoader<
async loadAssociatedEntityAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedSelectedFields
>,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedSelectedFields
>,
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
TAssociatedEntity
>
>(
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
fieldIdentifyingAssociatedEntity: keyof TFields,
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy,
TAssociatedSelectedFields
TAssociatedPrivacyPolicy
>,
queryContext: EntityQueryContext = this.entity
.getViewerContext()
@@ -64,52 +55,6 @@ export default class EntityAssociationLoader<
return await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID);
}
/**
* Load many entities associated with this entity, often referred to as entites belonging
* to this entity. In a relational database, the field in the foreign entity is a
* foreign key to the ID of this entity. Also commonly referred to as a has many relationship,
* where this entity has many associated entities.
* @param associatedEntityClass - class of the associated entities
* @param associatedEntityFieldContainingThisID - field of associated entity which contains the ID of this entity
* @param queryContext - query context in which to perform the load
*/
async loadManyAssociatedEntitiesAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity
>
>(
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy
>,
associatedEntityFieldContainingThisID: keyof TAssociatedFields,
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const thisID = this.entity.getID();
const loader = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext);
return await loader.loadManyByFieldEqualingAsync(
associatedEntityFieldContainingThisID,
thisID as any
);
}
/**
* Load an associated entity identified by a field value of this entity. In a relational database,
* the field in this entity is a foreign key to a unique field of the associated entity.
@@ -121,29 +66,21 @@ export default class EntityAssociationLoader<
async loadAssociatedEntityByFieldEqualingAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedSelectedFields
>,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedSelectedFields
>,
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
TAssociatedEntity
>
>(
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
fieldIdentifyingAssociatedEntity: keyof TFields,
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy,
TAssociatedSelectedFields
TAssociatedPrivacyPolicy
>,
associatedEntityLookupByField: keyof TAssociatedFields,
queryContext: EntityQueryContext = this.entity
@@ -164,52 +101,6 @@ export default class EntityAssociationLoader<
);
}
/**
* Load many associated entities identified by a field value of this entity. In a relational database,
* the field in this entity refers to a field of the associated entity.
* @param fieldIdentifyingAssociatedEntity - field of this entity containing the value with which to look up associated entities
* @param associatedEntityClass - class of the associated entities
* @param associatedEntityLookupByField - field of associated entities with which to look up the associated entities
* @param queryContext - query context in which to perform the load
*/
async loadManyAssociatedEntitiesByFieldEqualingAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity
>
>(
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy
>,
associatedEntityLookupByField: keyof TAssociatedFields,
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
const loader = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext);
return await loader.loadManyByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any
);
}
/**
* Load an associated entity by folding a sequence of {@link EntityLoadThroughDirective}. At each
* fold step, load an associated entity identified by a field value of the current fold value.
@@ -217,29 +108,15 @@ export default class EntityAssociationLoader<
* @param queryContext - query context in which to perform the loads
*/
async loadAssociatedEntityThroughAsync<
TEntityRoot extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicyRoot extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntityRoot>,
TFields2,
TID2,
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<
TFields2,
TID2,
TViewerContext,
TEntity2,
TSelectedFields2
>,
TSelectedFields2 extends keyof TFields2 = keyof TFields2
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>
>(
loadDirectives: [
EntityLoadThroughDirective<
TViewerContext,
TFields,
TFields2,
TID2,
TEntity2,
TPrivacyPolicy2,
TSelectedFields,
TSelectedFields2
>
EntityLoadThroughDirective<TViewerContext, TFields, TFields2, TID2, TEntity2, TPrivacyPolicy2>
],
queryContext?: EntityQueryContext
): Promise<Result<TEntity2> | null>;
@@ -251,28 +128,16 @@ export default class EntityAssociationLoader<
* @param queryContext - query context in which to perform the loads
*/
async loadAssociatedEntityThroughAsync<
TEntityRoot extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicyRoot extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntityRoot>,
TFields2,
TID2,
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<
TFields2,
TID2,
TViewerContext,
TEntity2,
TSelectedFields2
>,
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>,
TFields3,
TID3,
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext, TSelectedFields3>,
TPrivacyPolicy3 extends EntityPrivacyPolicy<
TFields3,
TID3,
TViewerContext,
TEntity3,
TSelectedFields3
>,
TSelectedFields2 extends keyof TFields2 = keyof TFields2,
TSelectedFields3 extends keyof TFields3 = keyof TFields3
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext>,
TPrivacyPolicy3 extends EntityPrivacyPolicy<TFields3, TID3, TViewerContext, TEntity3>
>(
loadDirectives: [
EntityLoadThroughDirective<
@@ -281,9 +146,7 @@ export default class EntityAssociationLoader<
TFields2,
TID2,
TEntity2,
TPrivacyPolicy2,
TSelectedFields,
TSelectedFields2
TPrivacyPolicy2
>,
EntityLoadThroughDirective<
TViewerContext,
@@ -291,9 +154,7 @@ export default class EntityAssociationLoader<
TFields3,
TID3,
TEntity3,
TPrivacyPolicy3,
TSelectedFields2,
TSelectedFields3
TPrivacyPolicy3
>
],
queryContext?: EntityQueryContext
@@ -306,39 +167,20 @@ export default class EntityAssociationLoader<
* @param queryContext - query context in which to perform the loads
*/
async loadAssociatedEntityThroughAsync<
TEntityRoot extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicyRoot extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntityRoot>,
TFields2,
TID2,
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<
TFields2,
TID2,
TViewerContext,
TEntity2,
TSelectedFields2
>,
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext>,
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>,
TFields3,
TID3,
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext, TSelectedFields3>,
TPrivacyPolicy3 extends EntityPrivacyPolicy<
TFields3,
TID3,
TViewerContext,
TEntity3,
TSelectedFields3
>,
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext>,
TPrivacyPolicy3 extends EntityPrivacyPolicy<TFields3, TID3, TViewerContext, TEntity3>,
TFields4,
TID4,
TEntity4 extends ReadonlyEntity<TFields4, TID4, TViewerContext, TSelectedFields4>,
TPrivacyPolicy4 extends EntityPrivacyPolicy<
TFields4,
TID4,
TViewerContext,
TEntity4,
TSelectedFields4
>,
TSelectedFields2 extends keyof TFields2 = keyof TFields2,
TSelectedFields3 extends keyof TFields3 = keyof TFields3,
TSelectedFields4 extends keyof TFields4 = keyof TFields4
TEntity4 extends ReadonlyEntity<TFields4, TID4, TViewerContext>,
TPrivacyPolicy4 extends EntityPrivacyPolicy<TFields4, TID4, TViewerContext, TEntity4>
>(
loadDirective: [
EntityLoadThroughDirective<
@@ -347,9 +189,7 @@ export default class EntityAssociationLoader<
TFields2,
TID2,
TEntity2,
TPrivacyPolicy2,
TSelectedFields,
TSelectedFields2
TPrivacyPolicy2
>,
EntityLoadThroughDirective<
TViewerContext,
@@ -357,9 +197,7 @@ export default class EntityAssociationLoader<
TFields3,
TID3,
TEntity3,
TPrivacyPolicy3,
TSelectedFields2,
TSelectedFields3
TPrivacyPolicy3
>,
EntityLoadThroughDirective<
TViewerContext,
@@ -367,9 +205,7 @@ export default class EntityAssociationLoader<
TFields4,
TID4,
TEntity4,
TPrivacyPolicy4,
TSelectedFields3,
TSelectedFields4
TPrivacyPolicy4
>
],
queryContext?: EntityQueryContext
@@ -382,22 +218,22 @@ export default class EntityAssociationLoader<
* @param queryContext - query context in which to perform the loads
*/
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any>[],
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any>[],
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null>;
): Promise<Result<ReadonlyEntity<any, any, any>> | null>;
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any>[],
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any>[],
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null> {
let currentEntity: ReadonlyEntity<any, any, any, any> = this.entity;
): Promise<Result<ReadonlyEntity<any, any, any>> | null> {
let currentEntity: ReadonlyEntity<any, any, any> = this.entity;
for (const loadDirective of loadDirectives) {
const {
associatedEntityClass,
fieldIdentifyingAssociatedEntity,
associatedEntityLookupByField,
} = loadDirective;
let associatedEntityResult: Result<ReadonlyEntity<any, any, any, any>> | null;
let associatedEntityResult: Result<ReadonlyEntity<any, any, any>> | null;
if (associatedEntityLookupByField) {
associatedEntityResult = await currentEntity
.associationLoader()
@@ -438,21 +274,13 @@ export interface EntityLoadThroughDirective<
TFields,
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedSelectedFields
>,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields,
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
TAssociatedEntity
>
> {
/**
* Class of entity to load at this step.
@@ -462,14 +290,13 @@ export interface EntityLoadThroughDirective<
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy,
TAssociatedSelectedFields
TAssociatedPrivacyPolicy
>;
/**
* Field of the current entity with which to load an instance of associatedEntityClass.
*/
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>;
fieldIdentifyingAssociatedEntity: keyof TFields;
/**
* Field by which to load the instance of associatedEntityClass. If not provided, the
+36 -47
View File
@@ -1,11 +1,17 @@
import { IEntityClass } from './Entity';
import EntityCacheAdapter from './EntityCacheAdapter';
import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
import EntityLoaderFactory from './EntityLoaderFactory';
import EntityMutatorFactory from './EntityMutatorFactory';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider';
import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider';
import IEntityQueryContextProvider from './IEntityQueryContextProvider';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import EntityTableDataCoordinator from './internal/EntityTableDataCoordinator';
import EntityDataManager from './internal/EntityDataManager';
import ReadThroughEntityCache from './internal/ReadThroughEntityCache';
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
export interface IPrivacyPolicyClass<TPrivacyPolicy> {
@@ -19,89 +25,72 @@ export default class EntityCompanion<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
// defined as properties so that they can be accessed from tests
private readonly databaseAdapter: EntityDatabaseAdapter<TFields>;
private readonly cacheAdapter: EntityCacheAdapter<TFields>;
private readonly entityLoaderFactory: EntityLoaderFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>;
private readonly entityMutatorFactory: EntityMutatorFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>;
constructor(
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
private readonly tableDataCoordinator: EntityTableDataCoordinator<TFields>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
entityConfiguration: EntityConfiguration<TFields>,
databaseAdapterProvider: IEntityDatabaseAdapterProvider,
cacheAdapterProvider: IEntityCacheAdapterProvider,
PrivacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>,
private readonly queryContextProvider: IEntityQueryContextProvider,
metricsAdapter: IEntityMetricsAdapter
) {
this.databaseAdapter = databaseAdapterProvider.getDatabaseAdapter(entityConfiguration);
this.cacheAdapter = cacheAdapterProvider.getCacheAdapter(entityConfiguration);
const dataManager = new EntityDataManager(
this.databaseAdapter,
new ReadThroughEntityCache(entityConfiguration, this.cacheAdapter),
queryContextProvider,
metricsAdapter
);
const privacyPolicy = new PrivacyPolicyClass();
this.entityLoaderFactory = new EntityLoaderFactory(
tableDataCoordinator.entityConfiguration.idField,
entityConfiguration,
entityClass,
privacyPolicy,
tableDataCoordinator.dataManager
dataManager
);
this.entityMutatorFactory = new EntityMutatorFactory(
tableDataCoordinator.entityConfiguration.idField,
entityConfiguration,
entityClass,
privacyPolicy,
this.entityLoaderFactory,
tableDataCoordinator.databaseAdapter,
this.databaseAdapter,
metricsAdapter
);
}
getLoaderFactory(): EntityLoaderFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
getLoaderFactory(): EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityLoaderFactory;
}
getMutatorFactory(): EntityMutatorFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
getMutatorFactory(): EntityMutatorFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityMutatorFactory;
}
/**
* Get the query context provider for this entity.
*/
getQueryContextProvider(): IEntityQueryContextProvider {
return this.tableDataCoordinator.getQueryContextProvider();
return this.queryContextProvider;
}
}
+22 -93
View File
@@ -7,7 +7,6 @@ import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider';
import IEntityQueryContextProvider from './IEntityQueryContextProvider';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import EntityTableDataCoordinator from './internal/EntityTableDataCoordinator';
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
import { computeIfAbsent } from './utils/collections/maps';
@@ -56,55 +55,18 @@ export interface CacheAdapterFlavorDefinition {
* Definition for constructing a companion for an entity. Defines the core set of objects
* used to power the entity framework for a particular type of entity.
*/
export class EntityCompanionDefinition<
export interface EntityCompanionDefinition<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>;
readonly entityConfiguration: EntityConfiguration<TFields>;
readonly privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
readonly entitySelectedFields: TSelectedFields[];
constructor({
entityClass,
entityConfiguration,
privacyPolicyClass,
entitySelectedFields = Array.from(entityConfiguration.schema.keys()) as TSelectedFields[],
}: {
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>;
entityConfiguration: EntityConfiguration<TFields>;
privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
entitySelectedFields?: TSelectedFields[];
}) {
this.entityClass = entityClass;
this.entityConfiguration = entityConfiguration;
this.privacyPolicyClass = privacyPolicyClass;
this.entitySelectedFields = entitySelectedFields;
}
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>;
entityConfiguration: EntityConfiguration<TFields>;
databaseAdaptorFlavor: DatabaseAdapterFlavor;
cacheAdaptorFlavor: CacheAdapterFlavor;
privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
}
/**
@@ -117,13 +79,9 @@ export class EntityCompanionDefinition<
* {@link EntityCompanion} for each type of {@link Entity}.
*/
export default class EntityCompanionProvider {
private readonly companionMap: Map<
private readonly entityCompanionMap: Map<
string,
EntityCompanion<any, any, any, any, any, any>
> = new Map();
private readonly tableDataCoordinatorMap: Map<
string,
EntityTableDataCoordinator<any>
EntityCompanion<any, any, any, any, any>
> = new Map();
/**
@@ -149,60 +107,31 @@ export default class EntityCompanionProvider {
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
>(
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
entityCompanionDefinition: EntityCompanionDefinition<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>
): EntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
const tableDataCoordinator = this.getTableDataCoordinatorForEntity(
entityCompanionDefinition.entityConfiguration
);
return computeIfAbsent(this.companionMap, entityClass.name, () => {
return new EntityCompanion(
entityCompanionDefinition.entityClass,
tableDataCoordinator,
entityCompanionDefinition.privacyPolicyClass,
this.metricsAdapter
);
});
}
private getTableDataCoordinatorForEntity<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityTableDataCoordinator<TFields> {
return computeIfAbsent(this.tableDataCoordinatorMap, entityConfiguration.tableName, () => {
): EntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return computeIfAbsent(this.entityCompanionMap, entityClass.name, () => {
const entityDatabaseAdapterFlavor = this.databaseAdapterFlavors[
entityConfiguration.databaseAdapterFlavor
entityCompanionDefinition.databaseAdaptorFlavor
];
const entityCacheAdapterFlavor = this.cacheAdapterFlavors[
entityConfiguration.cacheAdapterFlavor
entityCompanionDefinition.cacheAdaptorFlavor
];
return new EntityTableDataCoordinator(
entityConfiguration,
return new EntityCompanion(
entityCompanionDefinition.entityClass,
entityCompanionDefinition.entityConfiguration,
entityDatabaseAdapterFlavor.adapterProvider,
entityCacheAdapterFlavor.cacheAdapterProvider,
entityCompanionDefinition.privacyPolicyClass,
entityDatabaseAdapterFlavor.queryContextProvider,
this.metricsAdapter
);
+5 -15
View File
@@ -1,10 +1,9 @@
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from './EntityCompanionProvider';
import { EntityFieldDefinition } from './EntityFields';
import { mapMap, invertMap, reduceMap } from './utils/collections/maps';
/**
* The data storage configuration for a type of Entity. Contains information relating to IDs,
* cachable fields, field mappings, and types of cache and database adapter.
* cachable fields, and field mappings.
*/
export default class EntityConfiguration<TFields> {
readonly idField: keyof TFields;
@@ -12,33 +11,24 @@ export default class EntityConfiguration<TFields> {
readonly cacheableKeys: ReadonlySet<keyof TFields>;
readonly cacheKeyVersion: number;
readonly schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>;
readonly schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>;
readonly entityToDBFieldsKeyMapping: ReadonlyMap<keyof TFields, string>;
readonly dbToEntityFieldsKeyMapping: ReadonlyMap<string, keyof TFields>;
readonly databaseAdapterFlavor: DatabaseAdapterFlavor;
readonly cacheAdapterFlavor: CacheAdapterFlavor;
constructor({
idField,
tableName,
schema,
cacheKeyVersion = 0,
databaseAdapterFlavor,
cacheAdapterFlavor,
}: {
idField: keyof TFields;
tableName: string;
schema: Record<keyof TFields, EntityFieldDefinition>;
schema: Record<keyof TFields, EntityFieldDefinition<any>>;
cacheKeyVersion?: number;
databaseAdapterFlavor: DatabaseAdapterFlavor;
cacheAdapterFlavor: CacheAdapterFlavor;
}) {
this.idField = idField;
this.tableName = tableName;
this.cacheKeyVersion = cacheKeyVersion;
this.databaseAdapterFlavor = databaseAdapterFlavor;
this.cacheAdapterFlavor = cacheAdapterFlavor;
// external schema is a Record to typecheck that all fields have FieldDefinitions,
// but internally the most useful representation is a map for lookups
@@ -53,7 +43,7 @@ export default class EntityConfiguration<TFields> {
}
private static computeCacheableKeys<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlySet<keyof TFields> {
return reduceMap(
schema,
@@ -68,7 +58,7 @@ export default class EntityConfiguration<TFields> {
}
private static computeEntityToDBFieldsKeyMapping<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlyMap<keyof TFields, string> {
return mapMap(schema, (v) => v.columnName);
}
+5 -20
View File
@@ -11,26 +11,12 @@ export class EntityNotFoundError<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
N extends keyof TFields,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>,
N extends keyof TFields
> extends EntityError {
constructor(
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
fieldName: N,
fieldValue: TFields[N]
) {
@@ -42,8 +28,7 @@ export class EntityNotAuthorizedError<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> extends EntityError {
public readonly entityClassName: string;
+33 -11
View File
@@ -1,6 +1,17 @@
export abstract class EntityFieldDefinition {
export interface FieldValidator<T> {
/**
* Validation to apply for this field before an entity is created or updated.
* Only applies to fields changed during a mutation; any existing fields will
* not be re-validated in order to allow for changing of validator logic.
*/
write?: (value: T) => Promise<void>;
}
export abstract class EntityFieldDefinition<T> {
readonly columnName: string;
readonly cache: boolean;
readonly validator: FieldValidator<T>;
/**
*
* @param columnName - Column name in the database.
@@ -8,19 +19,30 @@ export abstract class EntityFieldDefinition {
* used to derive a cache key for the cache entry. If true, this column must be able uniquely
* identify the entity.
*/
constructor({ columnName, cache = false }: { columnName: string; cache?: boolean }) {
constructor({
columnName,
cache = false,
validator = {},
}: {
columnName: string;
cache?: boolean;
validator?: FieldValidator<T>;
}) {
this.columnName = columnName;
this.cache = cache;
this.validator = validator;
}
}
export class StringField extends EntityFieldDefinition {}
export class StringField extends EntityFieldDefinition<string> {}
export class UUIDField extends StringField {}
export class DateField extends EntityFieldDefinition {}
export class BooleanField extends EntityFieldDefinition {}
export class NumberField extends EntityFieldDefinition {}
export class StringArrayField extends EntityFieldDefinition {}
export class JSONObjectField extends EntityFieldDefinition {}
export class EnumField extends EntityFieldDefinition {}
export class JSONArrayField extends EntityFieldDefinition {}
export class MaybeJSONArrayField extends EntityFieldDefinition {}
export class DateField extends EntityFieldDefinition<Date> {}
export class BooleanField extends EntityFieldDefinition<boolean> {}
export class NumberField extends EntityFieldDefinition<number> {}
export class StringArrayField extends EntityFieldDefinition<string[]> {}
export class JSONObjectField<T extends object> extends EntityFieldDefinition<T> {}
export class EnumField<T> extends EntityFieldDefinition<T> {}
export class JSONArrayField<T> extends EntityFieldDefinition<T[]> {}
export class MaybeJSONArrayField<TArray, TNotArray> extends EntityFieldDefinition<
TArray[] | TNotArray
> {}
+20 -29
View File
@@ -3,6 +3,7 @@ import invariant from 'invariant';
import EnforcingEntityLoader from './EnforcingEntityLoader';
import { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import { FieldEqualityCondition, QuerySelectionModifiers } from './EntityDatabaseAdapter';
import { EntityNotFoundError } from './EntityErrors';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
@@ -20,30 +21,22 @@ export default class EntityLoader<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly viewerContext: TViewerContext,
private readonly queryContext: EntityQueryContext,
private readonly idField: keyof TFields,
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly privacyPolicy: TPrivacyPolicy,
private readonly dataManager: EntityDataManager<TFields>
private readonly entityDataManager: EntityDataManager<TFields>
) {}
/**
@@ -51,14 +44,7 @@ export default class EntityLoader<
* guaranteed to be the values of successful results (or null for some loader methods),
* and will throw otherwise.
*/
enforcing(): EnforcingEntityLoader<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
enforcing(): EnforcingEntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new EnforcingEntityLoader(this);
}
@@ -73,7 +59,7 @@ export default class EntityLoader<
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
const fieldValuesToObjects = await this.dataManager.loadManyByFieldEqualingAsync(
const fieldValuesToObjects = await this.entityDataManager.loadManyByFieldEqualingAsync(
this.queryContext,
fieldName,
fieldValues
@@ -142,7 +128,9 @@ export default class EntityLoader<
const entityResults = await this.loadManyByIDsAsync([id]);
const entityResult = entityResults.get(id);
if (entityResult === undefined) {
return result(new EntityNotFoundError(this.entityClass, this.idField, id as any));
return result(
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id as any)
);
}
return entityResult;
}
@@ -153,7 +141,7 @@ export default class EntityLoader<
* @returns entity result for matching ID, or null if no entity exists for ID.
*/
async loadByIDNullableAsync(id: TID): Promise<Result<TEntity> | null> {
return await this.loadByFieldEqualingAsync(this.idField, id as any);
return await this.loadByFieldEqualingAsync(this.entityConfiguration.idField, id as any);
}
/**
@@ -164,13 +152,16 @@ export default class EntityLoader<
*/
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
const entityResults = ((await this.loadManyByFieldEqualingManyAsync(
this.idField,
this.entityConfiguration.idField,
ids as any
)) as any) as ReadonlyMap<TID, readonly Result<TEntity>[]>;
return mapMap(entityResults, (entityResultsForId, id) => {
const entityResult = entityResultsForId[0];
return (
entityResult ?? result(new EntityNotFoundError(this.entityClass, this.idField, id as any))
entityResult ??
result(
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id as any)
)
);
});
}
@@ -193,7 +184,7 @@ export default class EntityLoader<
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
const fieldValuesArray = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
const fieldValuesArray = await this.entityDataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
fieldEqualityOperands,
querySelectionModifiers
@@ -241,7 +232,7 @@ export default class EntityLoader<
bindings: any[] | object,
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
const fieldValuesArray = await this.dataManager.loadManyByRawWhereClauseAsync(
const fieldValuesArray = await this.entityDataManager.loadManyByRawWhereClauseAsync(
this.queryContext,
rawWhereClause,
bindings,
@@ -265,6 +256,6 @@ export default class EntityLoader<
* @param objectFields - entity data object to be invalidated
*/
async invalidateFieldsAsync(objectFields: Readonly<TFields>): Promise<void> {
await this.dataManager.invalidateObjectFieldsAsync(objectFields);
await this.entityDataManager.invalidateObjectFieldsAsync(objectFields);
}
}
+7 -14
View File
@@ -1,4 +1,5 @@
import { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import EntityLoader from './EntityLoader';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import { EntityQueryContext } from './EntityQueryContext';
@@ -13,25 +14,17 @@ export default class EntityLoaderFactory<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly idField: keyof TFields,
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly privacyPolicyClass: TPrivacyPolicy,
private readonly dataManager: EntityDataManager<TFields>
@@ -45,11 +38,11 @@ export default class EntityLoaderFactory<
forLoad(
viewerContext: TViewerContext,
queryContext: EntityQueryContext
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new EntityLoader(
viewerContext,
queryContext,
this.idField,
this.entityConfiguration,
this.entityClass,
this.privacyPolicyClass,
this.dataManager
+60 -87
View File
@@ -1,7 +1,9 @@
import { Result, asyncResult, result, enforceAsyncResult } from '@expo/results';
import invariant from 'invariant';
import _ from 'lodash';
import Entity, { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
import EntityLoaderFactory from './EntityLoaderFactory';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
@@ -14,27 +16,19 @@ abstract class BaseMutator<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends Entity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
protected readonly viewerContext: TViewerContext,
protected readonly queryContext: EntityQueryContext,
protected readonly idField: keyof TFields,
protected readonly entityConfiguration: EntityConfiguration<TFields>,
protected readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
protected readonly privacyPolicy: TPrivacyPolicy,
protected readonly entityLoaderFactory: EntityLoaderFactory<
@@ -42,12 +36,30 @@ abstract class BaseMutator<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
protected readonly databaseAdapter: EntityDatabaseAdapter<TFields>,
protected readonly metricsAdapter: IEntityMetricsAdapter
) {}
protected async validateFields(fields: Partial<TFields>): Promise<Result<Partial<TFields>>> {
const validatorResults = await asyncResult(
Promise.all(
Object.entries(fields).map(async ([fieldName, fieldValue]) => {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName as keyof TFields);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
const writeValidator = fieldDefinition.validator.write;
if (writeValidator) {
await writeValidator(fieldValue);
}
})
)
);
if (!validatorResults.ok) {
return result(validatorResults.reason);
}
return result(fields);
}
}
/**
@@ -57,16 +69,9 @@ export class CreateMutator<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
TEntity extends Entity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
private readonly fieldsForEntity: Partial<TFields> = {};
/**
@@ -99,9 +104,14 @@ export class CreateMutator<
}
private async createInternalAsync(): Promise<Result<TEntity>> {
const validatedFieldsResult = await this.validateFields(this.fieldsForEntity);
if (!validatedFieldsResult.ok) {
return result(validatedFieldsResult.reason);
}
const temporaryEntityForPrivacyCheck = new this.entityClass(this.viewerContext, ({
[this.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
...this.fieldsForEntity,
[this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
...validatedFieldsResult.value,
} as unknown) as TFields);
const authorizeCreateResult = await asyncResult(
@@ -117,7 +127,7 @@ export class CreateMutator<
const insertResult = await this.databaseAdapter.insertAsync(
this.queryContext,
this.fieldsForEntity
validatedFieldsResult.value
);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
@@ -135,16 +145,9 @@ export class UpdateMutator<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
TEntity extends Entity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
private readonly originalFieldsForEntity: Readonly<TFields>;
private readonly fieldsForEntity: TFields;
private readonly updatedFields: Partial<TFields> = {};
@@ -152,24 +155,10 @@ export class UpdateMutator<
constructor(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
idField: keyof TFields,
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityConfiguration: EntityConfiguration<TFields>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
privacyPolicy: TPrivacyPolicy,
entityLoaderFactory: EntityLoaderFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityLoaderFactory: EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
databaseAdapter: EntityDatabaseAdapter<TFields>,
metricsAdapter: IEntityMetricsAdapter,
fieldsForEntity: Readonly<TFields>
@@ -177,7 +166,7 @@ export class UpdateMutator<
super(
viewerContext,
queryContext,
idField,
entityConfiguration,
entityClass,
privacyPolicy,
entityLoaderFactory,
@@ -220,6 +209,11 @@ export class UpdateMutator<
}
private async updateInternalAsync(): Promise<Result<TEntity>> {
const validatedUpdatedFieldsResult = await this.validateFields(this.updatedFields);
if (!validatedUpdatedFieldsResult.ok) {
return result(validatedUpdatedFieldsResult.reason);
}
const entityAboutToBeUpdated = new this.entityClass(this.viewerContext, this.fieldsForEntity);
const authorizeUpdateResult = await asyncResult(
this.privacyPolicy.authorizeUpdateAsync(
@@ -234,9 +228,9 @@ export class UpdateMutator<
const updateResult = await this.databaseAdapter.updateAsync(
this.queryContext,
this.idField,
this.entityConfiguration.idField,
entityAboutToBeUpdated.getID(),
this.updatedFields
validatedUpdatedFieldsResult.value
);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
@@ -256,37 +250,16 @@ export class DeleteMutator<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
TEntity extends Entity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
constructor(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
idField: keyof TFields,
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityConfiguration: EntityConfiguration<TFields>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
privacyPolicy: TPrivacyPolicy,
entityLoaderFactory: EntityLoaderFactory<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityLoaderFactory: EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
databaseAdapter: EntityDatabaseAdapter<TFields>,
metricsAdapter: IEntityMetricsAdapter,
private readonly entity: TEntity
@@ -294,7 +267,7 @@ export class DeleteMutator<
super(
viewerContext,
queryContext,
idField,
entityConfiguration,
entityClass,
privacyPolicy,
entityLoaderFactory,
@@ -331,10 +304,10 @@ export class DeleteMutator<
const id = this.entity.getID();
await this.databaseAdapter.deleteAsync(this.queryContext, this.idField, id);
await this.databaseAdapter.deleteAsync(this.queryContext, this.entityConfiguration.idField, id);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
await entityLoader.invalidateFieldsAsync(this.entity.getAllDatabaseFields());
await entityLoader.invalidateFieldsAsync(this.entity.getAllFields());
return result();
}
+13 -21
View File
@@ -1,4 +1,5 @@
import Entity, { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
import EntityLoaderFactory from './EntityLoaderFactory';
import { CreateMutator, UpdateMutator, DeleteMutator } from './EntityMutator';
@@ -14,25 +15,17 @@ export default class EntityMutatorFactory<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends Entity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly idField: keyof TFields,
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly privacyPolicy: TPrivacyPolicy,
private readonly entityLoaderFactory: EntityLoaderFactory<
@@ -40,8 +33,7 @@ export default class EntityMutatorFactory<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly databaseAdapter: EntityDatabaseAdapter<TFields>,
private readonly metricsAdapter: IEntityMetricsAdapter
@@ -56,11 +48,11 @@ export default class EntityMutatorFactory<
forCreate(
viewerContext: TViewerContext,
queryContext: EntityQueryContext
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new CreateMutator(
viewerContext,
queryContext,
this.idField,
this.entityConfiguration,
this.entityClass,
this.privacyPolicy,
this.entityLoaderFactory,
@@ -78,17 +70,17 @@ export default class EntityMutatorFactory<
forUpdate(
existingEntity: TEntity,
queryContext: EntityQueryContext
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new UpdateMutator(
existingEntity.getViewerContext(),
queryContext,
this.idField,
this.entityConfiguration,
this.entityClass,
this.privacyPolicy,
this.entityLoaderFactory,
this.databaseAdapter,
this.metricsAdapter,
existingEntity.getAllDatabaseFields()
existingEntity.getAllFields()
);
}
@@ -100,11 +92,11 @@ export default class EntityMutatorFactory<
forDelete(
existingEntity: TEntity,
queryContext: EntityQueryContext
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new DeleteMutator(
existingEntity.getViewerContext(),
queryContext,
this.idField,
this.entityConfiguration,
this.entityClass,
this.privacyPolicy,
this.entityLoaderFactory,
+11 -21
View File
@@ -14,23 +14,18 @@ export type EntityPrivacyPolicyEvaluator<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> =
| {
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE;
}
| {
mode: EntityPrivacyPolicyEvaluationMode.DRY_RUN;
denyHandler: (
error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity, TSelectedFields>
) => void;
denyHandler: (error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity>) => void;
}
| {
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG;
denyHandler: (
error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity, TSelectedFields>
) => void;
denyHandler: (error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity>) => void;
};
export enum EntityAuthorizationAction {
@@ -65,36 +60,31 @@ export default abstract class EntityPrivacyPolicy<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> {
protected readonly createRules: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
TEntity
>[] = [];
protected readonly readRules: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
TEntity
>[] = [];
protected readonly updateRules: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
TEntity
>[] = [];
protected readonly deleteRules: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
TEntity
>[] = [];
/**
@@ -107,7 +97,7 @@ export default abstract class EntityPrivacyPolicy<
*/
protected getPrivacyPolicyEvaluator(
_viewerContext: TViewerContext
): EntityPrivacyPolicyEvaluator<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
): EntityPrivacyPolicyEvaluator<TFields, TID, TViewerContext, TEntity> {
return {
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
};
@@ -202,7 +192,7 @@ export default abstract class EntityPrivacyPolicy<
}
private async authorizeForRulesetAsync(
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
entity: TEntity,
@@ -254,7 +244,7 @@ export default abstract class EntityPrivacyPolicy<
}
private async authorizeForRulesetInnerAsync(
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
entity: TEntity,
+17 -85
View File
@@ -1,5 +1,4 @@
import invariant from 'invariant';
import { pick } from 'lodash';
import { IEntityClass } from './Entity';
import EntityAssociationLoader from './EntityAssociationLoader';
@@ -16,14 +15,8 @@ import ViewerContext from './ViewerContext';
* - Entities representing SQL views.
* - Entities representing immutable tables.
*/
export default abstract class ReadonlyEntity<
TFields,
TID,
TViewerContext extends ViewerContext,
TSelectedFields extends keyof TFields = keyof TFields
> {
export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extends ViewerContext> {
private readonly id: TID;
private readonly rawFields: Readonly<Pick<TFields, TSelectedFields>>;
/**
* Constructs an instance of an Entity.
@@ -34,17 +27,13 @@ export default abstract class ReadonlyEntity<
*/
constructor(
private readonly viewerContext: TViewerContext,
private readonly databaseFields: Readonly<TFields>
private readonly rawFields: Readonly<TFields>
) {
const idField = (this.constructor as any).getCompanionDefinition().entityConfiguration
.idField as keyof Pick<TFields, TSelectedFields>;
const id = databaseFields[idField];
.idField as keyof TFields;
const id = rawFields[idField];
invariant(id, 'must provide ID to create an entity');
this.id = id as any;
const entitySelectedFields = (this.constructor as any).getCompanionDefinition()
.entitySelectedFields as (keyof TFields)[];
this.rawFields = pick(databaseFields, entitySelectedFields);
}
toString(): string {
@@ -68,13 +57,7 @@ export default abstract class ReadonlyEntity<
/**
* @returns {@link EntityAssociationLoader} for this entity
*/
associationLoader(): EntityAssociationLoader<
TFields,
TID,
TViewerContext,
this,
TSelectedFields
> {
associationLoader(): EntityAssociationLoader<TFields, TID, TViewerContext, this> {
return new EntityAssociationLoader(this);
}
@@ -83,26 +66,17 @@ export default abstract class ReadonlyEntity<
* @param fieldName - the field to get
* @returns the value of the field or undefined if not loaded with that field
*/
getField<K extends keyof Pick<TFields, TSelectedFields>>(
fieldName: K
): Pick<TFields, TSelectedFields>[K] {
getField<K extends keyof TFields>(fieldName: K): TFields[K] {
return this.rawFields[fieldName];
}
/**
* @returns all underlying fields from this entity's data
*/
getAllFields(): Readonly<Pick<TFields, TSelectedFields>> {
getAllFields(): Readonly<TFields> {
return { ...this.rawFields };
}
/**
* @returns all underlying fields from this entity's database data
*/
getAllDatabaseFields(): Readonly<TFields> {
return { ...this.databaseFields };
}
/**
* Get the regular (non-transactional) query context for this entity.
* @param viewerContext - viewer context of calling user
@@ -112,24 +86,10 @@ export default abstract class ReadonlyEntity<
TMID,
TMViewerContext extends ViewerContext,
TMViewerContext2 extends TMViewerContext,
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
viewerContext: TMViewerContext2
): EntityQueryContext {
return viewerContext
@@ -149,24 +109,10 @@ export default abstract class ReadonlyEntity<
TMID,
TMViewerContext extends ViewerContext,
TMViewerContext2 extends TMViewerContext,
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
viewerContext: TMViewerContext2,
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<TResult>
): Promise<TResult> {
@@ -186,30 +132,16 @@ export default abstract class ReadonlyEntity<
TMID,
TMViewerContext extends ViewerContext,
TMViewerContext2 extends TMViewerContext,
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
this: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>,
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
viewerContext: TMViewerContext2,
queryContext: EntityQueryContext = viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): EntityLoader<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
): EntityLoader<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
return viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getLoaderFactory()
+4 -25
View File
@@ -29,32 +29,11 @@ export default class ViewerContext {
TMFields,
TMID,
TMViewerContext extends ViewerContext,
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
TMPrivacyPolicy extends EntityPrivacyPolicy<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMSelectedFields
>,
TMSelectedFields extends keyof TMFields = keyof TMFields
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
>(
entityClass: IEntityClass<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
>
): ViewerScopedEntityCompanion<
TMFields,
TMID,
TMViewerContext,
TMEntity,
TMPrivacyPolicy,
TMSelectedFields
> {
entityClass: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>
): ViewerScopedEntityCompanion<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
return this.viewerScopedEntityCompanionProvider.getViewerScopedCompanionForEntity(
entityClass,
entityClass.getCompanionDefinition()
@@ -14,15 +14,8 @@ export default class ViewerScopedEntityCompanion<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly entityCompanion: EntityCompanion<
@@ -30,8 +23,7 @@ export default class ViewerScopedEntityCompanion<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly viewerContext: TViewerContext
) {}
@@ -44,8 +36,7 @@ export default class ViewerScopedEntityCompanion<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
> {
return new ViewerScopedEntityLoaderFactory(
this.entityCompanion.getLoaderFactory(),
@@ -61,8 +52,7 @@ export default class ViewerScopedEntityCompanion<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
> {
return new ViewerScopedEntityMutatorFactory(
this.entityCompanion.getMutatorFactory(),
@@ -25,40 +25,18 @@ export default class ViewerScopedEntityCompanionProvider {
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
>(
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
entityCompanionDefinition: EntityCompanionDefinition<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>
): ViewerScopedEntityCompanion<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
): ViewerScopedEntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return new ViewerScopedEntityCompanion(
this.entityCompanionProvider.getCompanionForEntity(entityClass, entityCompanionDefinition),
this.viewerContext as TViewerContext
@@ -12,15 +12,8 @@ export default class ViewerScopedEntityLoaderFactory<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly entityLoaderFactory: EntityLoaderFactory<
@@ -28,15 +21,14 @@ export default class ViewerScopedEntityLoaderFactory<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly viewerContext: TViewerContext
) {}
forLoad(
queryContext: EntityQueryContext
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
}
}
@@ -12,15 +12,8 @@ export default class ViewerScopedEntityMutatorFactory<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
> {
constructor(
private readonly entityMutatorFactory: EntityMutatorFactory<
@@ -28,29 +21,28 @@ export default class ViewerScopedEntityMutatorFactory<
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
TPrivacyPolicy
>,
private readonly viewerContext: TViewerContext
) {}
forCreate(
queryContext: EntityQueryContext
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityMutatorFactory.forCreate(this.viewerContext, queryContext);
}
forUpdate(
existingEntity: TEntity,
queryContext: EntityQueryContext
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityMutatorFactory.forUpdate(existingEntity, queryContext);
}
forDelete(
existingEntity: TEntity,
queryContext: EntityQueryContext
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
return this.entityMutatorFactory.forDelete(existingEntity, queryContext);
}
}
+8 -6
View File
@@ -96,8 +96,6 @@ const testEntityConfiguration = new EntityConfiguration<TestEntityFields>({
columnName: 'custom_id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
@@ -136,11 +134,13 @@ class SimpleTestDenyUpdateEntity extends Entity<TestEntityFields, string, Viewer
}
}
const simpleTestDenyUpdateEntityCompanion = new EntityCompanionDefinition({
const simpleTestDenyUpdateEntityCompanion = {
entityClass: SimpleTestDenyUpdateEntity,
entityConfiguration: testEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: SimpleTestDenyUpdateEntityPrivacyPolicy,
});
};
class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
@@ -154,8 +154,10 @@ class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, Viewer
}
}
const simpleTestDenyDeleteEntityCompanion = new EntityCompanionDefinition({
const simpleTestDenyDeleteEntityCompanion = {
entityClass: SimpleTestDenyDeleteEntity,
entityConfiguration: testEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: SimpleTestDenyDeleteEntityPrivacyPolicy,
});
};
@@ -1,7 +1,6 @@
import { enforceAsyncResult } from '@expo/results';
import EntityAssociationLoader from '../EntityAssociationLoader';
import { enforceResultsAsync } from '../entityUtils';
import TestEntity from '../testfixtures/TestEntity';
import TestEntity2 from '../testfixtures/TestEntity2';
import TestViewerContext from '../testfixtures/TestViewerContext';
@@ -27,28 +26,8 @@ describe(EntityAssociationLoader, () => {
});
});
describe('loadManyAssociatedEntitiesAsync', () => {
it('loads many associated entities referencing this entity', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity.associationLoader().loadManyAssociatedEntitiesAsync(TestEntity, 'stringField')
);
expect(loaded).toHaveLength(2);
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
expect(loaded.find((e) => e.getID() === testOtherEntity2.getID())).not.toBeUndefined();
});
});
describe('loadAssociatedEntityByFieldEqualingAsync', () => {
it('loads associated entity by field equaling', async () => {
describe('loadRelatedEntityByFieldEqualingAsync', () => {
it('loads associated entities by field equaling', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testOtherEntity = await enforceAsyncResult(
@@ -78,32 +57,6 @@ describe(EntityAssociationLoader, () => {
});
});
describe('loadManyAssociatedEntitiesByFieldEqualingAsync', () => {
it('loads many associated entities by field equaling', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity
.associationLoader()
.loadManyAssociatedEntitiesByFieldEqualingAsync(
'customIdField',
TestEntity,
'stringField'
)
);
expect(loaded).toHaveLength(2);
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
expect(loaded.find((e) => e.getID() === testOtherEntity2.getID())).not.toBeUndefined();
});
});
describe('loadAssociatedEntityThroughAsync', () => {
it('chain loads associated entities', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
@@ -86,7 +86,7 @@ class BlahEntityPrivacyPolicy extends EntityPrivacyPolicy<
];
}
const blahCompanion = new EntityCompanionDefinition({
const blahCompanion = {
entityClass: BlahEntity,
entityConfiguration: new EntityConfiguration<BlahFields>({
idField: 'id',
@@ -100,11 +100,11 @@ const blahCompanion = new EntityCompanionDefinition({
columnName: 'owner_id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
}),
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: BlahEntityPrivacyPolicy,
});
};
it('runs through a common workflow', async () => {
// will be one entity companion provider for each request, so
@@ -1,24 +1,26 @@
import { instance, mock, when } from 'ts-mockito';
import { instance, mock } from 'ts-mockito';
import EntityCompanion from '../EntityCompanion';
import EntityLoaderFactory from '../EntityLoaderFactory';
import EntityMutatorFactory from '../EntityMutatorFactory';
import EntityTableDataCoordinator from '../internal/EntityTableDataCoordinator';
import IEntityQueryContextProvider from '../IEntityQueryContextProvider';
import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
import TestEntity, {
TestEntityPrivacyPolicy,
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapterProvider from '../utils/testing/StubDatabaseAdapterProvider';
describe(EntityCompanion, () => {
it('correctly instantiates mutator and loader factories', () => {
const tableDataCoordinatorMock = mock(EntityTableDataCoordinator);
when(tableDataCoordinatorMock.entityConfiguration).thenReturn(testEntityConfiguration);
const companion = new EntityCompanion(
TestEntity,
instance(tableDataCoordinatorMock),
testEntityConfiguration,
new StubDatabaseAdapterProvider(),
new NoCacheStubCacheAdapterProvider(),
TestEntityPrivacyPolicy,
instance(mock<IEntityQueryContextProvider>()),
instance(mock<IEntityMetricsAdapter>())
);
expect(companion.getLoaderFactory()).toBeInstanceOf(EntityLoaderFactory);
@@ -22,8 +22,6 @@ const blahConfiguration = new EntityConfiguration<BlahFields>({
columnName: 'hello',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
class Blah1Entity extends Entity<BlahFields, string, ViewerContext> {
@@ -63,20 +61,24 @@ class NoOpTest2PrivacyPolicy extends EntityPrivacyPolicy<
Blah2Entity
> {}
const blah1CompanionDefinition = new EntityCompanionDefinition({
const blah1CompanionDefinition = {
entityClass: Blah1Entity,
entityConfiguration: blahConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: NoOpTest1PrivacyPolicy,
});
};
const blah2CompanionDefinition = new EntityCompanionDefinition({
const blah2CompanionDefinition = {
entityClass: Blah2Entity,
entityConfiguration: blahConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: NoOpTest2PrivacyPolicy,
});
};
describe(EntityCompanionProvider, () => {
it('returns different instances for different entity types, but share table data coordinators', () => {
it('returns different instances for different entity types', () => {
const entityCompanionProvider = createUnitTestEntityCompanionProvider();
const companion1 = entityCompanionProvider.getCompanionForEntity(
Blah1Entity,
@@ -89,6 +91,5 @@ describe(EntityCompanionProvider, () => {
);
expect(companion1).not.toEqual(companion2);
expect(companion1['tableDataCoordinator']).toEqual(companion2['tableDataCoordinator']);
});
});
@@ -1,4 +1,3 @@
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, StringField } from '../EntityFields';
@@ -28,15 +27,11 @@ describe(EntityConfiguration, () => {
columnName: 'unique_but_not_cacheable',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
it('returns correct fields', () => {
expect(blahEntityConfiguration.idField).toEqual('id');
expect(blahEntityConfiguration.tableName).toEqual('blah_table');
expect(blahEntityConfiguration.databaseAdapterFlavor).toEqual(DatabaseAdapterFlavor.POSTGRES);
expect(blahEntityConfiguration.cacheAdapterFlavor).toEqual(CacheAdapterFlavor.REDIS);
});
it('filters cacheable fields', () => {
@@ -53,8 +48,6 @@ describe(EntityConfiguration, () => {
columnName: 'id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
expect(entityConfiguration.cacheKeyVersion).toEqual(0);
});
@@ -68,8 +61,6 @@ describe(EntityConfiguration, () => {
columnName: 'id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
cacheKeyVersion: 100,
});
expect(entityConfiguration.cacheKeyVersion).toEqual(100);
@@ -1,6 +1,6 @@
import { EntityFieldDefinition } from '../EntityFields';
class TestFieldDefinition extends EntityFieldDefinition {}
class TestFieldDefinition extends EntityFieldDefinition<any> {}
describe(EntityFieldDefinition, () => {
it('returns correct column name and defaults cache to false', () => {
@@ -12,4 +12,21 @@ describe(EntityFieldDefinition, () => {
expect(fieldDefinition2.columnName).toEqual('wat');
expect(fieldDefinition2.cache).toEqual(true);
});
it('defaults validator to no-op', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validator).toEqual({});
const fn = async (): Promise<void> => {
throw new Error();
};
const fieldDefinition2 = new TestFieldDefinition({
columnName: 'wat',
cache: true,
validator: {
write: fn,
},
});
expect(fieldDefinition2.validator.write).toEqual(fn);
});
});
@@ -63,7 +63,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManager
@@ -144,7 +144,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManager
@@ -206,7 +206,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManager
@@ -225,7 +225,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManagerInstance
@@ -247,7 +247,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManagerInstance
@@ -280,7 +280,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManagerInstance
@@ -309,7 +309,7 @@ describe(EntityLoader, () => {
const entityLoader = new EntityLoader(
viewerContext,
queryContext,
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManagerInstance
@@ -30,6 +30,11 @@ import TestEntity, {
TestEntityPrivacyPolicy,
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import ValidationTestEntity, {
ValidationTestEntityPrivacyPolicy,
ValidationTestFields,
validationTestEntityConfiguration,
} from '../testfixtures/ValidationTestEntity';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider';
@@ -73,13 +78,13 @@ const createEntityMutatorFactory = (
metricsAdapter
);
const entityLoaderFactory = new EntityLoaderFactory(
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
dataManager
);
const entityMutatorFactory = new EntityMutatorFactory(
testEntityConfiguration.idField,
testEntityConfiguration,
TestEntity,
privacyPolicy,
entityLoaderFactory,
@@ -95,7 +100,7 @@ const createEntityMutatorFactory = (
};
describe(EntityMutatorFactory, () => {
it('creates entities and checks privacy', async () => {
it('creates entities checks privacy', async () => {
const viewerContext = mock<ViewerContext>();
const queryContext = mock<EntityQueryContext>();
const { privacyPolicy, entityMutatorFactory } = createEntityMutatorFactory([
@@ -246,6 +251,73 @@ describe(EntityMutatorFactory, () => {
expect(entities2).toHaveLength(2);
});
it('returns error when field not valid', async () => {
const viewerContext = mock<ViewerContext>();
const queryContext = mock<EntityQueryContext>();
const privacyPolicy = new ValidationTestEntityPrivacyPolicy();
const databaseAdapter = new StubDatabaseAdapter<ValidationTestFields>(
validationTestEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
validationTestEntityConfiguration,
new Map([[validationTestEntityConfiguration.tableName, []]])
)
);
const metricsAdapter = new NoOpEntityMetricsAdapter();
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(validationTestEntityConfiguration);
const entityCache = new ReadThroughEntityCache<ValidationTestFields>(
validationTestEntityConfiguration,
cacheAdapter
);
const dataManager = new EntityDataManager(
databaseAdapter,
entityCache,
StubQueryContextProvider,
metricsAdapter
);
const entityLoaderFactory = new EntityLoaderFactory(
validationTestEntityConfiguration,
ValidationTestEntity,
privacyPolicy,
dataManager
);
const entityMutatorFactory = new EntityMutatorFactory(
validationTestEntityConfiguration,
ValidationTestEntity,
privacyPolicy,
entityLoaderFactory,
databaseAdapter,
metricsAdapter
);
const entityCreateInvalidResult = await entityMutatorFactory
.forCreate(viewerContext, queryContext)
.setField('numberOtherThanTen', 10)
.createAsync();
expect(entityCreateInvalidResult.ok).toBe(false);
expect(entityCreateInvalidResult.reason?.message).toEqual('should not be ten');
const entityCreateValidResult = await entityMutatorFactory
.forCreate(viewerContext, queryContext)
.setField('numberOtherThanTen', 11)
.createAsync();
expect(entityCreateValidResult.ok).toBe(true);
const entityUpdateInvalidResult = await entityMutatorFactory
.forUpdate(entityCreateValidResult.enforceValue(), queryContext)
.setField('numberOtherThanTen', 10)
.updateAsync();
expect(entityUpdateInvalidResult.ok).toBe(false);
expect(entityUpdateInvalidResult.reason?.message).toEqual('should not be ten');
const entityUpdateValidResult = await entityMutatorFactory
.forUpdate(entityCreateValidResult.enforceValue(), queryContext)
.setField('numberOtherThanTen', 12)
.updateAsync();
expect(entityUpdateValidResult.ok).toBe(true);
});
it('returns error result when not authorized to create', async () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
@@ -289,7 +361,7 @@ describe(EntityMutatorFactory, () => {
).thenReject(rejectionError);
const entityMutatorFactory = new EntityMutatorFactory(
simpleTestEntityConfiguration.idField,
simpleTestEntityConfiguration,
SimpleTestEntity,
instance(privacyPolicyMock),
entityLoaderFactory,
@@ -352,7 +424,7 @@ describe(EntityMutatorFactory, () => {
);
const entityMutatorFactory = new EntityMutatorFactory(
simpleTestEntityConfiguration.idField,
simpleTestEntityConfiguration,
SimpleTestEntity,
privacyPolicy,
entityLoaderFactory,
@@ -224,7 +224,7 @@ class EmptyPolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContext,
protected readonly deleteRules = [];
}
const blahEntityCompanionDefinition = new EntityCompanionDefinition({
const blahEntityCompanionDefinition = {
entityClass: BlahEntity,
entityConfiguration: new EntityConfiguration<BlahFields>({
idField: 'id',
@@ -234,11 +234,11 @@ const blahEntityCompanionDefinition = new EntityCompanionDefinition({
columnName: 'id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
}),
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: AlwaysDenyPolicy,
});
};
describe(EntityPrivacyPolicy, () => {
it('throws EntityNotAuthorizedError when deny', async () => {
@@ -1,132 +0,0 @@
import Entity from '../../Entity';
import {
EntityCompanionDefinition,
DatabaseAdapterFlavor,
CacheAdapterFlavor,
} from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField, EnumField, StringField } from '../../EntityFields';
import EntityPrivacyPolicy from '../../EntityPrivacyPolicy';
import ViewerContext from '../../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../../rules/AlwaysAllowPrivacyPolicyRule';
import { createUnitTestEntityCompanionProvider } from '../../utils/testing/createUnitTestEntityCompanionProvider';
describe('Two entities backed by the same table', () => {
test('load by different types', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const one = await OneTestEntity.creator(viewerContext)
.setField('entity_type', EntityType.ONE)
.enforceCreateAsync();
const two = await TwoTestEntity.creator(viewerContext)
.setField('entity_type', EntityType.TWO)
.setField('other_field', 'blah')
.enforceCreateAsync();
expect(one).toBeInstanceOf(OneTestEntity);
expect(two).toBeInstanceOf(TwoTestEntity);
await expect(
TwoTestEntity.loader(viewerContext).enforcing().loadByIDAsync(one.getID())
).rejects.toThrowError('TwoTestEntity must be instantiated with two data');
await expect(
OneTestEntity.loader(viewerContext).enforcing().loadByIDAsync(two.getID())
).rejects.toThrowError('OneTestEntity must be instantiated with one data');
});
});
enum EntityType {
ONE,
TWO,
}
interface TestFields {
id: string;
other_field: string;
entity_type: EntityType;
}
type OneTestFields = 'id' | 'entity_type';
type TwoTestFields = 'id' | 'other_field' | 'entity_type';
const testEntityConfiguration = new EntityConfiguration<TestFields>({
idField: 'id',
tableName: 'entities',
schema: {
id: new UUIDField({
columnName: 'custom_id',
cache: true,
}),
other_field: new StringField({
columnName: 'other_field',
}),
entity_type: new EnumField({
columnName: 'entity_type',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected readonly readRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly createRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly updateRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly deleteRules = [new AlwaysAllowPrivacyPolicyRule()];
}
class OneTestEntity extends Entity<TestFields, string, ViewerContext, OneTestFields> {
constructor(viewerContext: ViewerContext, rawFields: Readonly<TestFields>) {
if (rawFields.entity_type !== EntityType.ONE) {
throw new Error('OneTestEntity must be instantiated with one data');
}
super(viewerContext, rawFields);
}
static getCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
ViewerContext,
OneTestEntity,
TestEntityPrivacyPolicy,
OneTestFields
> {
return oneTestEntityCompanion;
}
}
class TwoTestEntity extends Entity<TestFields, string, ViewerContext, TwoTestFields> {
constructor(viewerContext: ViewerContext, rawFields: Readonly<TestFields>) {
if (rawFields.entity_type !== EntityType.TWO) {
throw new Error('TwoTestEntity must be instantiated with two data');
}
super(viewerContext, rawFields);
}
static getCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
ViewerContext,
TwoTestEntity,
TestEntityPrivacyPolicy,
TwoTestFields
> {
return twoTestEntityCompanion;
}
}
const oneTestEntityCompanion = new EntityCompanionDefinition({
entityClass: OneTestEntity,
entityConfiguration: testEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
entitySelectedFields: ['id', 'entity_type'],
});
const twoTestEntityCompanion = new EntityCompanionDefinition({
entityClass: TwoTestEntity,
entityConfiguration: testEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
entitySelectedFields: ['id', 'other_field', 'entity_type'],
});
@@ -1,166 +0,0 @@
import Entity from '../../Entity';
import {
EntityCompanionDefinition,
DatabaseAdapterFlavor,
CacheAdapterFlavor,
} from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField, StringField } from '../../EntityFields';
import EntityPrivacyPolicy from '../../EntityPrivacyPolicy';
import ViewerContext from '../../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../../rules/AlwaysAllowPrivacyPolicyRule';
import { createUnitTestEntityCompanionProvider } from '../../utils/testing/createUnitTestEntityCompanionProvider';
describe('Two entities backed by the same table', () => {
test('mutate through different types and keep consistent cache and dataloader', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const entity1 = await OneTestEntity.creator(viewerContext)
.setField('fake_field', 'hello')
.enforceCreateAsync();
expect(entity1).toBeInstanceOf(OneTestEntity);
const entity2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(entity2).toBeInstanceOf(TwoTestEntity);
const updated2 = await TwoTestEntity.updater(entity2)
.setField('fake_field', 'world')
.setField('other_field', 'wat')
.enforceUpdateAsync();
expect(updated2.getAllFields()).toMatchObject({
id: updated2.getID(),
other_field: 'wat',
fake_field: 'world',
});
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(loaded1.getAllFields()).toMatchObject({
id: updated2.getID(),
fake_field: 'world',
});
});
test('cached field that differs between the two to test invalidation', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const entity = await TwoTestEntity.creator(viewerContext)
.setField('fake_field', 'hello')
.setField('other_field', 'huh')
.enforceCreateAsync();
const loadedEntity = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loadedEntity?.getAllFields()).toMatchObject({
id: entity.getID(),
fake_field: 'hello',
other_field: 'huh',
});
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity.getID());
await OneTestEntity.updater(loaded1).setField('fake_field', 'world').enforceUpdateAsync();
const loaded2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loaded2?.getAllFields()).toMatchObject({
id: entity.getID(),
fake_field: 'world',
other_field: 'huh',
});
const loaded22 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('fake_field', 'world');
expect(loaded22?.getAllFields()).toMatchObject({
id: entity.getID(),
fake_field: 'world',
other_field: 'huh',
});
});
});
interface TestFields {
id: string;
other_field: string;
fake_field: string;
}
type OneTestFields = 'id' | 'fake_field';
type TwoTestFields = 'id' | 'other_field' | 'fake_field';
const testEntityConfiguration = new EntityConfiguration<TestFields>({
idField: 'id',
tableName: 'entities',
schema: {
id: new UUIDField({
columnName: 'custom_id',
cache: true,
}),
other_field: new StringField({
columnName: 'other_field',
cache: true,
}),
fake_field: new StringField({
columnName: 'fake_field',
cache: true,
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected readonly readRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly createRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly updateRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly deleteRules = [new AlwaysAllowPrivacyPolicyRule()];
}
class OneTestEntity extends Entity<TestFields, string, ViewerContext, OneTestFields> {
static getCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
ViewerContext,
OneTestEntity,
TestEntityPrivacyPolicy,
OneTestFields
> {
return oneTestEntityCompanion;
}
}
class TwoTestEntity extends Entity<TestFields, string, ViewerContext, TwoTestFields> {
static getCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
ViewerContext,
TwoTestEntity,
TestEntityPrivacyPolicy,
TwoTestFields
> {
return twoTestEntityCompanion;
}
}
const oneTestEntityCompanion = new EntityCompanionDefinition({
entityClass: OneTestEntity,
entityConfiguration: testEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
entitySelectedFields: ['id', 'fake_field'],
});
const twoTestEntityCompanion = new EntityCompanionDefinition({
entityClass: TwoTestEntity,
entityConfiguration: testEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
entitySelectedFields: ['id', 'other_field', 'fake_field'],
});
@@ -1,41 +0,0 @@
import EntityCacheAdapter from '../EntityCacheAdapter';
import EntityConfiguration from '../EntityConfiguration';
import EntityDatabaseAdapter from '../EntityDatabaseAdapter';
import IEntityCacheAdapterProvider from '../IEntityCacheAdapterProvider';
import IEntityDatabaseAdapterProvider from '../IEntityDatabaseAdapterProvider';
import IEntityQueryContextProvider from '../IEntityQueryContextProvider';
import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
import EntityDataManager from './EntityDataManager';
import ReadThroughEntityCache from './ReadThroughEntityCache';
/**
* Responsible for orchestrating fetching and caching of entity data from a
* table. Note that one instance is shared amongst all entities that read from
* the table to ensure cross-entity data consistency.
*/
export default class EntityTableDataCoordinator<TFields> {
readonly databaseAdapter: EntityDatabaseAdapter<TFields>;
readonly cacheAdapter: EntityCacheAdapter<TFields>;
readonly dataManager: EntityDataManager<TFields>;
constructor(
readonly entityConfiguration: EntityConfiguration<TFields>,
databaseAdapterProvider: IEntityDatabaseAdapterProvider,
cacheAdapterProvider: IEntityCacheAdapterProvider,
private readonly queryContextProvider: IEntityQueryContextProvider,
metricsAdapter: IEntityMetricsAdapter
) {
this.databaseAdapter = databaseAdapterProvider.getDatabaseAdapter(entityConfiguration);
this.cacheAdapter = cacheAdapterProvider.getCacheAdapter(entityConfiguration);
this.dataManager = new EntityDataManager(
this.databaseAdapter,
new ReadThroughEntityCache(entityConfiguration, this.cacheAdapter),
queryContextProvider,
metricsAdapter
);
}
getQueryContextProvider(): IEntityQueryContextProvider {
return this.queryContextProvider;
}
}
@@ -115,7 +115,6 @@ export default class ReadThroughEntityCache<TFields> {
if (objects.length > 1) {
// multiple objects received for what was supposed to be a unique query, don't add to return map nor cache
// TODO(wschurman): emit or throw here since console may not be available
// eslint-disable-next-line no-console
console.warn(
`unique key ${fieldName} in ${this.entityConfiguration.tableName} returned multiple rows for ${fieldValue}`
);
@@ -1,4 +1,3 @@
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField, StringField } from '../../EntityFields';
import {
@@ -38,8 +37,6 @@ const blahEntityConfiguration = new EntityConfiguration<BlahT>({
columnName: 'transform_write',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
describe(getDatabaseFieldForEntityField, () => {
@@ -1,7 +1,6 @@
import { verify, deepEqual, mock, instance, when, anything } from 'ts-mockito';
import EntityCacheAdapter from '../../EntityCacheAdapter';
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField } from '../../EntityFields';
import ReadThroughEntityCache, { CacheStatus } from '../ReadThroughEntityCache';
@@ -17,8 +16,6 @@ const makeEntityConfiguration = (cacheIdField: boolean): EntityConfiguration<Bla
schema: {
id: new UUIDField({ columnName: 'id', cache: cacheIdField }),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
const createIdFetcher = (ids: string[]) => async <N extends keyof BlahFields>(
@@ -10,9 +10,8 @@ export default class AlwaysAllowPrivacyPolicyRule<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
@@ -10,9 +10,8 @@ export default class AlwaysDenyPrivacyPolicyRule<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
@@ -10,9 +10,8 @@ export default class AlwaysSkipPrivacyPolicyRule<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
@@ -39,8 +39,7 @@ export default abstract class PrivacyPolicyRule<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> {
abstract async evaluateAsync(
viewerContext: TViewerContext,
@@ -22,8 +22,6 @@ export const dateIDTestEntityConfiguration = new EntityConfiguration<DateIDTestF
columnName: 'custom_id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
export class DateIDTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
@@ -58,8 +56,10 @@ export default class DateIDTestEntity extends Entity<DateIDTestFields, Date, Vie
}
}
export const dateIDTestEntityCompanion = new EntityCompanionDefinition({
export const dateIDTestEntityCompanion = {
entityClass: DateIDTestEntity,
entityConfiguration: dateIDTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: DateIDTestEntityPrivacyPolicy,
});
};
@@ -14,8 +14,6 @@ export type SimpleTestFields = {
id: string;
};
export type SimpleTestFieldSelection = keyof SimpleTestFields;
export const simpleTestEntityConfiguration = new EntityConfiguration<SimpleTestFields>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
@@ -24,75 +22,44 @@ export const simpleTestEntityConfiguration = new EntityConfiguration<SimpleTestF
columnName: 'custom_id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
SimpleTestEntity
> {
protected readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
];
protected readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
];
protected readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
];
protected readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
];
}
export default class SimpleTestEntity extends Entity<
SimpleTestFields,
string,
ViewerContext,
SimpleTestFieldSelection
> {
export default class SimpleTestEntity extends Entity<SimpleTestFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
SimpleTestFields,
string,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
SimpleTestFieldSelection
SimpleTestEntityPrivacyPolicy
> {
return testEntityCompanion;
}
}
export const testEntityCompanion = new EntityCompanionDefinition({
export const testEntityCompanion = {
entityClass: SimpleTestEntity,
entityConfiguration: simpleTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: SimpleTestEntityPrivacyPolicy,
});
};
@@ -41,8 +41,6 @@ export const testEntityConfiguration = new EntityConfiguration<TestFields>({
columnName: 'date_field',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
@@ -105,8 +103,10 @@ export default class TestEntity extends Entity<TestFields, string, ViewerContext
}
}
export const testEntityCompanion = new EntityCompanionDefinition({
export const testEntityCompanion = {
entityClass: TestEntity,
entityConfiguration: testEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: TestEntityPrivacyPolicy,
});
};
@@ -26,8 +26,6 @@ export const testEntity2Configuration = new EntityConfiguration<Test2Fields>({
columnName: 'foreign_key',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
export class TestEntity2PrivacyPolicy extends EntityPrivacyPolicy<
@@ -62,8 +60,10 @@ export default class TestEntity2 extends Entity<Test2Fields, string, ViewerConte
}
}
export const testEntity2Companion = new EntityCompanionDefinition({
export const testEntity2Companion = {
entityClass: TestEntity2,
entityConfiguration: testEntity2Configuration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: TestEntity2PrivacyPolicy,
});
};
@@ -22,8 +22,6 @@ export const numberKeyEntityConfiguration = new EntityConfiguration<NumberKeyFie
columnName: 'custom_id',
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});
export class NumberKeyPrivacyPolicy extends EntityPrivacyPolicy<
@@ -58,8 +56,10 @@ export default class NumberKeyEntity extends Entity<NumberKeyFields, number, Vie
}
}
export const numberKeyEntityCompanion = new EntityCompanionDefinition({
export const numberKeyEntityCompanion = {
entityClass: NumberKeyEntity,
entityConfiguration: numberKeyEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: NumberKeyPrivacyPolicy,
});
};
@@ -0,0 +1,72 @@
import Entity from '../Entity';
import {
DatabaseAdapterFlavor,
CacheAdapterFlavor,
EntityCompanionDefinition,
} from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, NumberField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type ValidationTestFields = {
id: string;
numberOtherThanTen: number;
};
export const validationTestEntityConfiguration = new EntityConfiguration<ValidationTestFields>({
idField: 'id',
tableName: 'validation_test_entity',
schema: {
id: new UUIDField({
columnName: 'custom_id',
}),
numberOtherThanTen: new NumberField({
columnName: 'not_ten',
validator: {
async write(value) {
if (value === 10) {
throw new Error('should not be ten');
}
},
},
}),
},
});
export class ValidationTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
ValidationTestFields,
string,
ViewerContext,
ValidationTestEntity
> {
protected readonly readRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly createRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly updateRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly deleteRules = [new AlwaysAllowPrivacyPolicyRule()];
}
export default class ValidationTestEntity extends Entity<
ValidationTestFields,
string,
ViewerContext
> {
static getCompanionDefinition(): EntityCompanionDefinition<
ValidationTestFields,
string,
ViewerContext,
ValidationTestEntity,
ValidationTestEntityPrivacyPolicy
> {
return validationTestEntityCompanion;
}
}
export const validationTestEntityCompanion = {
entityClass: ValidationTestEntity,
entityConfiguration: validationTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: ValidationTestEntityPrivacyPolicy,
};
@@ -7,8 +7,7 @@ export interface Case<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> {
viewerContext: TViewerContext;
queryContext: EntityQueryContext;
@@ -19,9 +18,8 @@ type CaseMap<
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> = Map<string, () => Promise<Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>>>;
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
> = Map<string, () => Promise<Case<TFields, TID, TViewerContext, TEntity>>>;
/**
* Useful for defining test cases that have async preconditions.
@@ -30,18 +28,17 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
>(
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>,
{
allowCases = new Map(),
skipCases = new Map(),
denyCases = new Map(),
}: {
allowCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
skipCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
denyCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
allowCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
skipCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
denyCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
}
): void => {
describe(privacyPolicyRule.constructor.name, () => {
@@ -87,27 +84,26 @@ export const describePrivacyPolicyRule = <
TFields,
TID,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
>(
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>,
{
allowCases = [],
skipCases = [],
denyCases = [],
}: {
allowCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
skipCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
denyCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
allowCases?: Case<TFields, TID, TViewerContext, TEntity>[];
skipCases?: Case<TFields, TID, TViewerContext, TEntity>[];
denyCases?: Case<TFields, TID, TViewerContext, TEntity>[];
}
): void => {
const makeCasesMap = (
cases: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
): CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields> =>
cases: Case<TFields, TID, TViewerContext, TEntity>[]
): CaseMap<TFields, TID, TViewerContext, TEntity> =>
cases.reduce(
(
acc: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
testCase: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
acc: CaseMap<TFields, TID, TViewerContext, TEntity>,
testCase: Case<TFields, TID, TViewerContext, TEntity>,
index
) => {
acc.set(`case ${index}`, async () => testCase);
@@ -14,7 +14,7 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
}
}
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
public getFieldTransformerMap(): FieldTransformerMap {
return new Map();
}
@@ -48,22 +48,15 @@ export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields
}
export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
cache: Map<string, Readonly<object>> = new Map();
getCacheAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityCacheAdapter<TFields> {
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration, this.cache);
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration);
}
}
class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
constructor(
entityConfiguration: EntityConfiguration<TFields>,
readonly cache: Map<string, Readonly<object>>
) {
super(entityConfiguration);
}
cache: Map<string, Readonly<object>> = new Map();
public getFieldTransformerMap(): FieldTransformerMap {
return new Map();
@@ -109,7 +102,7 @@ class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFie
public async invalidateManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
fieldValues: readonly TFields[N][]
): Promise<void> {
fieldValues.forEach((fieldValue) => {
const cacheKey = this.createCacheKey(fieldName, fieldValue);