mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6222f0f464 | ||
|
|
e3b2b235f8 | ||
|
|
d9603298e0 | ||
|
|
b22c734275 | ||
|
|
012c05d62d | ||
|
|
ead9561cc7 | ||
|
|
d1a6f13396 | ||
|
|
cca8fc1987 | ||
|
|
86aa5b291d | ||
|
|
4026fc1a61 | ||
|
|
a263e74b8a |
No files matched your search
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix null handling in load paths (try 2) ([#96](https://github.com/expo/entity/issues/96)) ([d960329](https://github.com/expo/entity/commit/d9603298e0cc5c3c2b0cca959876e9195555fb57))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure again ([#93](https://github.com/expo/entity/issues/93)) ([012c05d](https://github.com/expo/entity/commit/012c05d62d2ecd559eb331c6ca358052bfe66dc8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure ([#89](https://github.com/expo/entity/issues/89)) ([cca8fc1](https://github.com/expo/entity/commit/cca8fc19870057ebc7033081b59fab4a142fbf1a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to invalidate an entity manually ([#91](https://github.com/expo/entity/issues/91)) ([d1a6f13](https://github.com/expo/entity/commit/d1a6f1339648942f12b3fb605eb329aefe5cdd63))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ To release a new version, let's say `v1.0.0` for example:
|
||||
1. `git push origin v1.0.0`
|
||||
1. In GitHub release interface, create a new release from the tag, copy changelog changes to release description.
|
||||
1. `yarn clean && yarn tsc`
|
||||
1. In each public subpackage, run `npm publish`
|
||||
1. In each public subpackage, run `npm publish` and enter 2FA token.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.8.0"
|
||||
"version": "0.10.0"
|
||||
}
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-redis",
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Redis cache adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -32,6 +32,6 @@
|
||||
"ioredis": "^4.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.8.0"
|
||||
"@expo/entity": "^0.10.0"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex",
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Knex database adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -32,6 +32,6 @@
|
||||
"knex": "^0.21.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.8.0"
|
||||
"@expo/entity": "^0.10.0"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-example",
|
||||
"private": true,
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.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.8.0",
|
||||
"@expo/entity": "^0.10.0",
|
||||
"apollo-server-koa": "^2.16.1",
|
||||
"graphql": "^15.3.0",
|
||||
"koa": "^2.13.0",
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure again ([#93](https://github.com/expo/entity/issues/93)) ([012c05d](https://github.com/expo/entity/commit/012c05d62d2ecd559eb331c6ca358052bfe66dc8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure ([#89](https://github.com/expo/entity/issues/89)) ([cca8fc1](https://github.com/expo/entity/commit/cca8fc19870057ebc7033081b59fab4a142fbf1a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-full-integration-tests",
|
||||
"private": true,
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Full redis and knex integration tests for the entity framework",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
@@ -26,8 +26,8 @@
|
||||
"@expo/entity-database-adapter-knex": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.8.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.8.0",
|
||||
"@expo/entity-database-adapter-knex": "^0.8.0"
|
||||
"@expo/entity": "^0.10.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.10.0",
|
||||
"@expo/entity-database-adapter-knex": "^0.10.0"
|
||||
}
|
||||
}
|
||||
+3
-99
@@ -1,108 +1,12 @@
|
||||
import {
|
||||
EntityPrivacyPolicy,
|
||||
ViewerContext,
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
Entity,
|
||||
EntityCompanionDefinition,
|
||||
EntityConfiguration,
|
||||
DatabaseAdapterFlavor,
|
||||
CacheAdapterFlavor,
|
||||
UUIDField,
|
||||
EntityEdgeDeletionBehavior,
|
||||
} from '@expo/entity';
|
||||
import { ViewerContext } from '@expo/entity';
|
||||
import { RedisCacheAdapterContext } from '@expo/entity-cache-adapter-redis';
|
||||
import Redis from 'ioredis';
|
||||
import Knex from 'knex';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { createFullIntegrationTestEntityCompanionProvider } from '../testfixtures/createFullIntegrationTestEntityCompanionProvider';
|
||||
|
||||
interface ParentFields {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ChildFields {
|
||||
id: string;
|
||||
parent_id: string;
|
||||
}
|
||||
|
||||
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 ParentEntity extends Entity<ParentFields, string, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
ParentFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
ParentEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
> {
|
||||
return parentEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
class ChildEntity extends Entity<ChildFields, string, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
ChildFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
ChildEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
> {
|
||||
return childEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
const parentEntityConfiguration = new EntityConfiguration<ParentFields>({
|
||||
idField: 'id',
|
||||
tableName: 'parents',
|
||||
inboundEdges: [ChildEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
cache: true,
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
const childEntityConfiguration = new EntityConfiguration<ChildFields>({
|
||||
idField: 'id',
|
||||
tableName: 'children',
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
cache: true,
|
||||
}),
|
||||
parent_id: new UUIDField({
|
||||
columnName: 'parent_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: ParentEntity,
|
||||
edgeDeletionBehavior: EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE,
|
||||
},
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
const parentEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: ParentEntity,
|
||||
entityConfiguration: parentEntityConfiguration,
|
||||
privacyPolicyClass: TestEntityPrivacyPolicy,
|
||||
});
|
||||
|
||||
const childEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: ChildEntity,
|
||||
entityConfiguration: childEntityConfiguration,
|
||||
privacyPolicyClass: TestEntityPrivacyPolicy,
|
||||
});
|
||||
import ChildEntity from './entities/ChildEntity';
|
||||
import ParentEntity from './entities/ParentEntity';
|
||||
|
||||
async function createOrTruncatePostgresTables(knex: Knex): Promise<void> {
|
||||
await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); // for uuid_generate_v4()
|
||||
|
||||
+4
-4
@@ -67,7 +67,7 @@ const makeEntityClasses = async (knex: Knex, edgeDeletionBehavior: EntityEdgeDel
|
||||
const categoryEntityConfiguration = new EntityConfiguration<CategoryFields>({
|
||||
idField: 'id',
|
||||
tableName: categoriesTableName,
|
||||
inboundEdges: [OtherEntity],
|
||||
getInboundEdges: () => [OtherEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
@@ -77,7 +77,7 @@ const makeEntityClasses = async (knex: Knex, edgeDeletionBehavior: EntityEdgeDel
|
||||
columnName: 'parent_other_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: OtherEntity,
|
||||
getAssociatedEntityClass: () => OtherEntity,
|
||||
edgeDeletionBehavior,
|
||||
},
|
||||
}),
|
||||
@@ -95,7 +95,7 @@ const makeEntityClasses = async (knex: Knex, edgeDeletionBehavior: EntityEdgeDel
|
||||
const otherEntityConfiguration = new EntityConfiguration<OtherFields>({
|
||||
idField: 'id',
|
||||
tableName: othersTableName,
|
||||
inboundEdges: [CategoryEntity],
|
||||
getInboundEdges: () => [CategoryEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
@@ -105,7 +105,7 @@ const makeEntityClasses = async (knex: Knex, edgeDeletionBehavior: EntityEdgeDel
|
||||
columnName: 'parent_category_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: CategoryEntity,
|
||||
getAssociatedEntityClass: () => CategoryEntity,
|
||||
edgeDeletionBehavior,
|
||||
},
|
||||
}),
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
CacheAdapterFlavor,
|
||||
DatabaseAdapterFlavor,
|
||||
Entity,
|
||||
EntityCompanionDefinition,
|
||||
EntityConfiguration,
|
||||
EntityEdgeDeletionBehavior,
|
||||
EntityPrivacyPolicy,
|
||||
UUIDField,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
|
||||
import ParentEntity from './ParentEntity';
|
||||
|
||||
interface ChildFields {
|
||||
id: string;
|
||||
parent_id: string;
|
||||
}
|
||||
|
||||
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()];
|
||||
}
|
||||
|
||||
export default class ChildEntity extends Entity<ChildFields, string, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
ChildFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
ChildEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
> {
|
||||
return childEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
const childEntityConfiguration = new EntityConfiguration<ChildFields>({
|
||||
idField: 'id',
|
||||
tableName: 'children',
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
cache: true,
|
||||
}),
|
||||
parent_id: new UUIDField({
|
||||
columnName: 'parent_id',
|
||||
cache: true,
|
||||
association: {
|
||||
getAssociatedEntityClass: () => ParentEntity,
|
||||
edgeDeletionBehavior: EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE,
|
||||
},
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
const childEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: ChildEntity,
|
||||
entityConfiguration: childEntityConfiguration,
|
||||
privacyPolicyClass: TestEntityPrivacyPolicy,
|
||||
});
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
AlwaysAllowPrivacyPolicyRule,
|
||||
CacheAdapterFlavor,
|
||||
DatabaseAdapterFlavor,
|
||||
Entity,
|
||||
EntityCompanionDefinition,
|
||||
EntityConfiguration,
|
||||
EntityPrivacyPolicy,
|
||||
UUIDField,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
|
||||
import ChildEntity from './ChildEntity';
|
||||
|
||||
interface ParentFields {
|
||||
id: string;
|
||||
}
|
||||
|
||||
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()];
|
||||
}
|
||||
|
||||
export default class ParentEntity extends Entity<ParentFields, string, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
ParentFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
ParentEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
> {
|
||||
return parentEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
const parentEntityConfiguration = new EntityConfiguration<ParentFields>({
|
||||
idField: 'id',
|
||||
tableName: 'parents',
|
||||
getInboundEdges: () => [ChildEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
cache: true,
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
const parentEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: ParentEntity,
|
||||
entityConfiguration: parentEntityConfiguration,
|
||||
privacyPolicyClass: TestEntityPrivacyPolicy,
|
||||
});
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.10.0](https://github.com/expo/entity/compare/v0.9.1...v0.10.0) (2020-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix null handling in load paths (try 2) ([#96](https://github.com/expo/entity/issues/96)) ([d960329](https://github.com/expo/entity/commit/d9603298e0cc5c3c2b0cca959876e9195555fb57))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.9.1](https://github.com/expo/entity/compare/v0.9.0...v0.9.1) (2020-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure again ([#93](https://github.com/expo/entity/issues/93)) ([012c05d](https://github.com/expo/entity/commit/012c05d62d2ecd559eb331c6ca358052bfe66dc8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.9.0](https://github.com/expo/entity/compare/v0.8.1...v0.9.0) (2020-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* edge deletion behavior cyclic import structure ([#89](https://github.com/expo/entity/issues/89)) ([cca8fc1](https://github.com/expo/entity/commit/cca8fc19870057ebc7033081b59fab4a142fbf1a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to invalidate an entity manually ([#91](https://github.com/expo/entity/issues/91)) ([d1a6f13](https://github.com/expo/entity/commit/d1a6f1339648942f12b3fb605eb329aefe5cdd63))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.1](https://github.com/expo/entity/compare/v0.8.0...v0.8.1) (2020-10-09)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.8.0](https://github.com/expo/entity/compare/v0.7.1...v0.8.0) (2020-10-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity",
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "A privacy-first data model",
|
||||
"files": [
|
||||
"build",
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class EntityAssociationLoader<
|
||||
* @param queryContext - query context in which to perform the load
|
||||
*/
|
||||
async loadAssociatedEntityAsync<
|
||||
TIdentifyingField extends keyof Pick<TFields, TSelectedFields>,
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TAssociatedEntity extends ReadonlyEntity<
|
||||
@@ -40,7 +41,7 @@ export default class EntityAssociationLoader<
|
||||
>,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
>(
|
||||
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
|
||||
fieldIdentifyingAssociatedEntity: TIdentifyingField,
|
||||
associatedEntityClass: IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
@@ -54,14 +55,25 @@ export default class EntityAssociationLoader<
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
.getQueryContextProvider()
|
||||
.getQueryContext()
|
||||
): Promise<Result<TAssociatedEntity>> {
|
||||
): Promise<
|
||||
Result<null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity>
|
||||
> {
|
||||
const associatedEntityID = this.entity.getField(fieldIdentifyingAssociatedEntity);
|
||||
if (!associatedEntityID) {
|
||||
return result(null) as Result<
|
||||
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
|
||||
>;
|
||||
}
|
||||
|
||||
const loader = this.entity
|
||||
.getViewerContext()
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
.getLoaderFactory()
|
||||
.forLoad(queryContext);
|
||||
return await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID);
|
||||
|
||||
return (await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID)) as Result<
|
||||
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +173,9 @@ export default class EntityAssociationLoader<
|
||||
.getQueryContext()
|
||||
): Promise<Result<TAssociatedEntity> | null> {
|
||||
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
|
||||
if (!associatedFieldValue) {
|
||||
return null;
|
||||
}
|
||||
const loader = this.entity
|
||||
.getViewerContext()
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
@@ -215,6 +230,10 @@ export default class EntityAssociationLoader<
|
||||
.getQueryContext()
|
||||
): Promise<readonly Result<TAssociatedEntity>[]> {
|
||||
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
|
||||
if (!associatedFieldValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loader = this.entity
|
||||
.getViewerContext()
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
@@ -424,13 +443,19 @@ export default class EntityAssociationLoader<
|
||||
queryContext
|
||||
);
|
||||
} else {
|
||||
associatedEntityResult = await currentEntity
|
||||
const associatedEntityResultLocal = await currentEntity
|
||||
.associationLoader()
|
||||
.loadAssociatedEntityAsync(
|
||||
fieldIdentifyingAssociatedEntity,
|
||||
associatedEntityClass,
|
||||
queryContext
|
||||
);
|
||||
|
||||
if (associatedEntityResultLocal.ok && associatedEntityResultLocal.value === null) {
|
||||
associatedEntityResult = null;
|
||||
} else {
|
||||
associatedEntityResult = associatedEntityResultLocal;
|
||||
}
|
||||
}
|
||||
|
||||
if (!associatedEntityResult) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class EntityConfiguration<TFields> {
|
||||
readonly cacheableKeys: ReadonlySet<keyof TFields>;
|
||||
readonly cacheKeyVersion: number;
|
||||
|
||||
readonly inboundEdges: IEntityClass<any, any, any, any, any, any>[];
|
||||
readonly getInboundEdges: () => IEntityClass<any, any, any, any, any, any>[];
|
||||
readonly schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>;
|
||||
readonly entityToDBFieldsKeyMapping: ReadonlyMap<keyof TFields, string>;
|
||||
readonly dbToEntityFieldsKeyMapping: ReadonlyMap<string, keyof TFields>;
|
||||
@@ -25,7 +25,7 @@ export default class EntityConfiguration<TFields> {
|
||||
idField,
|
||||
tableName,
|
||||
schema,
|
||||
inboundEdges = [],
|
||||
getInboundEdges = () => [],
|
||||
cacheKeyVersion = 0,
|
||||
databaseAdapterFlavor,
|
||||
cacheAdapterFlavor,
|
||||
@@ -33,7 +33,7 @@ export default class EntityConfiguration<TFields> {
|
||||
idField: keyof TFields;
|
||||
tableName: string;
|
||||
schema: Record<keyof TFields, EntityFieldDefinition>;
|
||||
inboundEdges?: IEntityClass<any, any, any, any, any, any>[];
|
||||
getInboundEdges?: () => IEntityClass<any, any, any, any, any, any>[];
|
||||
cacheKeyVersion?: number;
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor;
|
||||
cacheAdapterFlavor: CacheAdapterFlavor;
|
||||
@@ -44,7 +44,7 @@ export default class EntityConfiguration<TFields> {
|
||||
this.databaseAdapterFlavor = databaseAdapterFlavor;
|
||||
this.cacheAdapterFlavor = cacheAdapterFlavor;
|
||||
|
||||
this.inboundEdges = inboundEdges;
|
||||
this.getInboundEdges = getInboundEdges;
|
||||
|
||||
// external schema is a Record to typecheck that all fields have FieldDefinitions,
|
||||
// but internally the most useful representation is a map for lookups
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface EntityAssociationDefinition<
|
||||
/**
|
||||
* Class of entity on the other end of this edge.
|
||||
*/
|
||||
associatedEntityClass: IEntityClass<
|
||||
getAssociatedEntityClass: () => IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
|
||||
@@ -284,10 +284,26 @@ export default class EntityLoader<
|
||||
await this.dataManager.invalidateObjectFieldsAsync(objectFields);
|
||||
}
|
||||
|
||||
async invalidateFieldsTransactionalAsync(objectFields: Readonly<TFields>): Promise<void> {
|
||||
await this.dataManager.invalidateObjectFieldsTransactionalAsync(
|
||||
this.queryContext,
|
||||
objectFields
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all caches for an entity. One potential use case would be to keep the entity
|
||||
* framework in sync with changes made to data outside of the framework.
|
||||
* @param entity - entity to be invalidated
|
||||
*/
|
||||
async invalidateEntityAsync(entity: TEntity): Promise<void> {
|
||||
await this.invalidateFieldsAsync(entity.getAllDatabaseFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap entity construction in try/catch and return Results. Used to prevent constructor errors
|
||||
* thrown for one entity instantiation from interfering with other entity instantiatons of the same query.
|
||||
* @param fieldsObject - array of entity data objects to construct entities
|
||||
* @param fieldsObjects - array of entity data objects to construct entities
|
||||
*/
|
||||
private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
|
||||
return fieldsObjects.map((fieldsObject) => {
|
||||
|
||||
@@ -205,6 +205,7 @@ export class CreateMutator<
|
||||
queryContext.appendPostCommitCallback(
|
||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
|
||||
);
|
||||
await entityLoader.invalidateFieldsTransactionalAsync(insertResult);
|
||||
|
||||
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
|
||||
const newEntity = await entityLoader
|
||||
@@ -395,6 +396,11 @@ export class UpdateMutator<
|
||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
entityLoader.invalidateFieldsTransactionalAsync(this.originalFieldsForEntity),
|
||||
entityLoader.invalidateFieldsTransactionalAsync(this.fieldsForEntity),
|
||||
]);
|
||||
|
||||
const unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
|
||||
const updatedEntity = await entityLoader
|
||||
.enforcing()
|
||||
@@ -566,6 +572,7 @@ export class DeleteMutator<
|
||||
queryContext.appendPostCommitCallback(
|
||||
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
|
||||
);
|
||||
await entityLoader.invalidateFieldsTransactionalAsync(this.entity.getAllDatabaseFields());
|
||||
|
||||
await this.executeMutationTriggersOrValidatorsAsync(
|
||||
this.mutationTriggers.afterDelete,
|
||||
@@ -629,7 +636,7 @@ export class DeleteMutator<
|
||||
TMSelectedFields
|
||||
>;
|
||||
const entityConfiguration = companionDefinition.entityConfiguration;
|
||||
const inboundEdges = entityConfiguration.inboundEdges;
|
||||
const inboundEdges = entityConfiguration.getInboundEdges();
|
||||
await Promise.all(
|
||||
inboundEdges.map(async (entityClass) => {
|
||||
return await mapMapAsync(
|
||||
@@ -640,8 +647,9 @@ export class DeleteMutator<
|
||||
return;
|
||||
}
|
||||
|
||||
const associatedConfiguration = association.associatedEntityClass.getCompanionDefinition()
|
||||
.entityConfiguration;
|
||||
const associatedConfiguration = association
|
||||
.getAssociatedEntityClass()
|
||||
.getCompanionDefinition().entityConfiguration;
|
||||
if (associatedConfiguration !== entityConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import EntityQueryContextProvider from './EntityQueryContextProvider';
|
||||
|
||||
export type PostCommitCallback = (...args: any) => Promise<any>;
|
||||
@@ -12,7 +14,7 @@ export type PostCommitCallback = (...args: any) => Promise<any>;
|
||||
export abstract class EntityQueryContext {
|
||||
constructor(private readonly queryInterface: any) {}
|
||||
|
||||
abstract isInTransaction(): boolean;
|
||||
abstract isInTransaction(): this is EntityTransactionalQueryContext;
|
||||
|
||||
getQueryInterface(): any {
|
||||
return this.queryInterface;
|
||||
@@ -31,7 +33,7 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
||||
super(queryInterface);
|
||||
}
|
||||
|
||||
isInTransaction(): boolean {
|
||||
isInTransaction(): this is EntityTransactionalQueryContext {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,6 +45,8 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
||||
}
|
||||
|
||||
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
||||
public readonly transactionID = uuidv4();
|
||||
|
||||
private readonly postCommitCallbacks: PostCommitCallback[] = [];
|
||||
|
||||
public appendPostCommitCallback(callback: PostCommitCallback): void {
|
||||
@@ -55,7 +59,7 @@ export class EntityTransactionalQueryContext extends EntityQueryContext {
|
||||
await Promise.all(callbacks.map((callback) => callback()));
|
||||
}
|
||||
|
||||
isInTransaction(): boolean {
|
||||
isInTransaction(): this is EntityTransactionalQueryContext {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,8 @@ describe(EnforcingEntityLoader, () => {
|
||||
const knownLoaderOnlyDifferences = [
|
||||
'enforcing',
|
||||
'invalidateFieldsAsync',
|
||||
'invalidateEntityAsync',
|
||||
'invalidateFieldsTransactionalAsync',
|
||||
'tryConstructEntities',
|
||||
];
|
||||
expect(loaderProperties).toEqual(expect.arrayContaining(knownLoaderOnlyDifferences));
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUn
|
||||
|
||||
describe(EntityAssociationLoader, () => {
|
||||
describe('loadAssociatedEntityAsync', () => {
|
||||
it('loads associated entities by ID', async () => {
|
||||
it('loads associated entities by ID and correctly handles a null value', async () => {
|
||||
const companionProvider = createUnitTestEntityCompanionProvider();
|
||||
const viewerContext = new TestViewerContext(companionProvider);
|
||||
const testOtherEntity = await enforceAsyncResult(
|
||||
@@ -24,6 +24,11 @@ describe(EntityAssociationLoader, () => {
|
||||
testEntity.associationLoader().loadAssociatedEntityAsync('stringField', TestEntity)
|
||||
);
|
||||
expect(loadedOther.getID()).toEqual(testOtherEntity.getID());
|
||||
|
||||
const loadedOther2 = await enforceAsyncResult(
|
||||
testEntity.associationLoader().loadAssociatedEntityAsync('nullableField', TestEntity)
|
||||
);
|
||||
expect(loadedOther2).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,6 +81,18 @@ describe(EntityAssociationLoader, () => {
|
||||
.loadAssociatedEntityByFieldEqualingAsync('stringField', TestEntity, 'customIdField');
|
||||
expect(loadedOtherResult).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when load-by field is null', async () => {
|
||||
const companionProvider = createUnitTestEntityCompanionProvider();
|
||||
const viewerContext = new TestViewerContext(companionProvider);
|
||||
const testEntity = await enforceAsyncResult(
|
||||
TestEntity.creator(viewerContext).setField('stringField', 'blah').createAsync()
|
||||
);
|
||||
const loadedOtherResult = await testEntity
|
||||
.associationLoader()
|
||||
.loadAssociatedEntityByFieldEqualingAsync('nullableField', TestEntity, 'customIdField');
|
||||
expect(loadedOtherResult).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadManyAssociatedEntitiesByFieldEqualingAsync', () => {
|
||||
@@ -102,6 +119,22 @@ describe(EntityAssociationLoader, () => {
|
||||
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
|
||||
expect(loaded.find((e) => e.getID() === testOtherEntity2.getID())).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns empty results when field being queried by is null', async () => {
|
||||
const companionProvider = createUnitTestEntityCompanionProvider();
|
||||
const viewerContext = new TestViewerContext(companionProvider);
|
||||
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
|
||||
const loaded = await enforceResultsAsync(
|
||||
testEntity
|
||||
.associationLoader()
|
||||
.loadManyAssociatedEntitiesByFieldEqualingAsync(
|
||||
'nullableField',
|
||||
TestEntity,
|
||||
'stringField'
|
||||
)
|
||||
);
|
||||
expect(loaded).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAssociatedEntityThroughAsync', () => {
|
||||
@@ -213,5 +246,22 @@ describe(EntityAssociationLoader, () => {
|
||||
]);
|
||||
expect(loaded2Result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when chain loading by field is null', async () => {
|
||||
const companionProvider = createUnitTestEntityCompanionProvider();
|
||||
const viewerContext = new TestViewerContext(companionProvider);
|
||||
|
||||
const testEntity = await enforceAsyncResult(
|
||||
TestEntity.creator(viewerContext).setField('nullableField', null).createAsync()
|
||||
);
|
||||
|
||||
const loadedResult = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
|
||||
{
|
||||
associatedEntityClass: TestEntity,
|
||||
fieldIdentifyingAssociatedEntity: 'nullableField',
|
||||
},
|
||||
]);
|
||||
expect(loadedResult).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -81,7 +81,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
const parentEntityConfiguration = new EntityConfiguration<ParentFields>({
|
||||
idField: 'id',
|
||||
tableName: 'parents',
|
||||
inboundEdges: [ChildEntity],
|
||||
getInboundEdges: () => [ChildEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
@@ -95,7 +95,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
const childEntityConfiguration = new EntityConfiguration<ChildFields>({
|
||||
idField: 'id',
|
||||
tableName: 'children',
|
||||
inboundEdges: [GrandChildEntity],
|
||||
getInboundEdges: () => [GrandChildEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
@@ -105,7 +105,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
columnName: 'parent_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: ParentEntity,
|
||||
getAssociatedEntityClass: () => ParentEntity,
|
||||
edgeDeletionBehavior,
|
||||
},
|
||||
}),
|
||||
@@ -126,7 +126,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
columnName: 'parent_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: ChildEntity,
|
||||
getAssociatedEntityClass: () => ChildEntity,
|
||||
edgeDeletionBehavior,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -36,6 +36,7 @@ describe(EntityLoader, () => {
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -43,6 +44,7 @@ describe(EntityLoader, () => {
|
||||
numberField: 3,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -112,6 +114,7 @@ describe(EntityLoader, () => {
|
||||
numberField: 4,
|
||||
testIndexedField: '4',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -119,6 +122,7 @@ describe(EntityLoader, () => {
|
||||
numberField: 4,
|
||||
testIndexedField: '5',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'blah',
|
||||
@@ -126,6 +130,7 @@ describe(EntityLoader, () => {
|
||||
numberField: 4,
|
||||
testIndexedField: '6',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -189,6 +194,7 @@ describe(EntityLoader, () => {
|
||||
testIndexedField: '1',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -260,6 +266,31 @@ describe(EntityLoader, () => {
|
||||
).once();
|
||||
});
|
||||
|
||||
it('invalidates upon invalidate by entity', async () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = StubQueryContextProvider.getQueryContext();
|
||||
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
|
||||
const dataManagerMock = mock(EntityDataManager);
|
||||
const dataManagerInstance = instance(dataManagerMock);
|
||||
|
||||
const entityMock = mock(TestEntity);
|
||||
when(entityMock.getAllDatabaseFields()).thenReturn({ customIdField: 'hello' } as any);
|
||||
const entityInstance = instance(entityMock);
|
||||
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManagerInstance
|
||||
);
|
||||
await entityLoader.invalidateEntityAsync(entityInstance);
|
||||
verify(
|
||||
dataManagerMock.invalidateObjectFieldsAsync(deepEqual({ customIdField: 'hello' }))
|
||||
).once();
|
||||
});
|
||||
|
||||
it('returns error result when not allowed', async () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = StubQueryContextProvider.getQueryContext();
|
||||
|
||||
@@ -310,6 +310,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -317,6 +318,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '5',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
const newEntity = await entityMutatorFactory
|
||||
@@ -336,6 +338,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -343,6 +346,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '5',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -372,6 +376,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -379,6 +384,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '5',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -409,6 +415,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -416,6 +423,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '5',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -441,6 +449,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -448,6 +457,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -484,6 +494,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -491,6 +502,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -528,6 +540,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -535,6 +548,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -573,6 +587,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -580,6 +595,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '4',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -609,6 +625,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -640,6 +657,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -674,6 +692,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -709,6 +728,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -734,6 +754,7 @@ describe(EntityMutatorFactory, () => {
|
||||
testIndexedField: '3',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const makeEntityClass = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => {
|
||||
const categoryEntityConfiguration = new EntityConfiguration<CategoryFields>({
|
||||
idField: 'id',
|
||||
tableName: 'categories',
|
||||
inboundEdges: [CategoryEntity],
|
||||
getInboundEdges: () => [CategoryEntity],
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
@@ -58,7 +58,7 @@ const makeEntityClass = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => {
|
||||
columnName: 'parent_category_id',
|
||||
cache: true,
|
||||
association: {
|
||||
associatedEntityClass: CategoryEntity,
|
||||
getAssociatedEntityClass: () => CategoryEntity,
|
||||
edgeDeletionBehavior,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -63,6 +63,7 @@ describe(ReadonlyEntity, () => {
|
||||
stringField: '3',
|
||||
numberField: 4,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
});
|
||||
expect(testEntity.getUniqueIdentifier()).not.toEqual(testEntity2.getUniqueIdentifier());
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import EntityDatabaseAdapter, {
|
||||
FieldEqualityCondition,
|
||||
QuerySelectionModifiers,
|
||||
} from '../EntityDatabaseAdapter';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import { EntityQueryContext, EntityTransactionalQueryContext } from '../EntityQueryContext';
|
||||
import EntityQueryContextProvider from '../EntityQueryContextProvider';
|
||||
import { partitionErrors } from '../entityUtils';
|
||||
import {
|
||||
@@ -15,6 +15,13 @@ import IEntityMetricsAdapter, { EntityMetricsLoadType } from '../metrics/IEntity
|
||||
import { computeIfAbsent, zipToMap } from '../utils/collections/maps';
|
||||
import ReadThroughEntityCache from './ReadThroughEntityCache';
|
||||
|
||||
type FieldDataLoader<TFields> = DataLoader<
|
||||
NonNullable<TFields[keyof TFields]>,
|
||||
readonly Readonly<TFields>[]
|
||||
>;
|
||||
|
||||
type FieldDataLoadersMap<TFields> = Map<keyof TFields, FieldDataLoader<TFields>>;
|
||||
|
||||
/**
|
||||
* A data manager is responsible for orchestrating multiple sources of entity
|
||||
* data including local caches, {@link EntityCacheAdapter}, and {@link EntityDatabaseAdapter}.
|
||||
@@ -22,9 +29,11 @@ import ReadThroughEntityCache from './ReadThroughEntityCache';
|
||||
* It is also responsible for invalidating all sources of data when mutated using {@link EntityMutator}.
|
||||
*/
|
||||
export default class EntityDataManager<TFields> {
|
||||
private readonly fieldDataLoaders: Map<
|
||||
keyof TFields,
|
||||
DataLoader<NonNullable<TFields[keyof TFields]>, readonly Readonly<TFields>[]>
|
||||
private readonly regularFieldDataLoaders: FieldDataLoadersMap<TFields> = new Map();
|
||||
|
||||
private readonly transactionalFieldDataLoaders: Map<
|
||||
string,
|
||||
FieldDataLoadersMap<TFields>
|
||||
> = new Map();
|
||||
|
||||
constructor(
|
||||
@@ -35,10 +44,10 @@ export default class EntityDataManager<TFields> {
|
||||
private readonly entityClassName: string
|
||||
) {}
|
||||
|
||||
private getFieldDataLoaderForFieldName<N extends keyof TFields>(
|
||||
private getRegularFieldDataLoderForFieldName<N extends keyof TFields>(
|
||||
fieldName: N
|
||||
): DataLoader<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> {
|
||||
return computeIfAbsent(this.fieldDataLoaders, fieldName, () => {
|
||||
): FieldDataLoader<TFields> {
|
||||
return computeIfAbsent(this.regularFieldDataLoaders, fieldName, () => {
|
||||
return new DataLoader(
|
||||
async (
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
@@ -53,16 +62,60 @@ export default class EntityDataManager<TFields> {
|
||||
});
|
||||
}
|
||||
|
||||
private getTransactionalFieldDataLoaderForFieldName<N extends keyof TFields>(
|
||||
queryContext: EntityTransactionalQueryContext,
|
||||
fieldName: N
|
||||
): FieldDataLoader<TFields> {
|
||||
const fieldDataLoaderMap = computeIfAbsent(
|
||||
this.transactionalFieldDataLoaders,
|
||||
queryContext.transactionID,
|
||||
() => new Map<keyof TFields, FieldDataLoader<TFields>>()
|
||||
);
|
||||
return computeIfAbsent(fieldDataLoaderMap, fieldName, () => {
|
||||
return new DataLoader(
|
||||
async (
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<readonly (readonly TFields[])[]> => {
|
||||
const objectMap = await this.loadManyForTransactionalDataLoaderByFieldEqualingAsync(
|
||||
fieldName,
|
||||
fieldValues,
|
||||
queryContext
|
||||
);
|
||||
return fieldValues.map((fv) => objectMap.get(fv) ?? []);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadManyForTransactionalDataLoaderByFieldEqualingAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[],
|
||||
queryContext: EntityTransactionalQueryContext
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Readonly<TFields>[]>> {
|
||||
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
|
||||
fieldValueCount: fieldValues.length,
|
||||
entityClassName: this.entityClassName,
|
||||
});
|
||||
return await this.databaseAdapter.fetchManyWhereAsync(queryContext, fieldName, fieldValues);
|
||||
}
|
||||
|
||||
private async loadManyForDataLoaderByFieldEqualingAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Readonly<TFields>[]>> {
|
||||
this.metricsAdapter.incrementDataManagerCacheLoadCount(fieldValues.length);
|
||||
this.metricsAdapter.incrementDataManagerCacheLoadCount({
|
||||
fieldValueCount: fieldValues.length,
|
||||
entityClassName: this.entityClassName,
|
||||
});
|
||||
|
||||
return await this.entityCache.readManyThroughAsync(
|
||||
fieldName,
|
||||
fieldValues,
|
||||
async (fetcherValues) => {
|
||||
this.metricsAdapter.incrementDataManagerDatabaseLoadCount(fieldValues.length);
|
||||
async (fetcherValues: readonly NonNullable<TFields[N]>[]) => {
|
||||
this.metricsAdapter.incrementDataManagerDatabaseLoadCount({
|
||||
fieldValueCount: fieldValues.length,
|
||||
entityClassName: this.entityClassName,
|
||||
});
|
||||
return await this.databaseAdapter.fetchManyWhereAsync(
|
||||
this.queryContextProvider.getQueryContext(),
|
||||
fieldName,
|
||||
@@ -97,13 +150,22 @@ export default class EntityDataManager<TFields> {
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Readonly<TFields>[]>> {
|
||||
// don't cache when in transaction, as rollbacks complicate things significantly
|
||||
if (queryContext.isInTransaction()) {
|
||||
return await this.databaseAdapter.fetchManyWhereAsync(queryContext, fieldName, fieldValues);
|
||||
const nullOrUndefinedValueIndex = fieldValues.findIndex(
|
||||
(value) => value === null || value === undefined
|
||||
);
|
||||
if (nullOrUndefinedValueIndex >= 0) {
|
||||
throw new Error(
|
||||
`Invalid load: ${this.entityClassName} (${fieldName} = ${fieldValues[nullOrUndefinedValueIndex]})`
|
||||
);
|
||||
}
|
||||
|
||||
this.metricsAdapter.incrementDataManagerDataloaderLoadCount(fieldValues.length);
|
||||
const dataLoader = this.getFieldDataLoaderForFieldName(fieldName);
|
||||
this.metricsAdapter.incrementDataManagerDataloaderLoadCount({
|
||||
fieldValueCount: fieldValues.length,
|
||||
entityClassName: this.entityClassName,
|
||||
});
|
||||
const dataLoader = queryContext.isInTransaction()
|
||||
? this.getTransactionalFieldDataLoaderForFieldName(queryContext, fieldName)
|
||||
: this.getRegularFieldDataLoderForFieldName(fieldName);
|
||||
const results = await dataLoader.loadMany(fieldValues);
|
||||
const [values, errors] = partitionErrors(results);
|
||||
if (errors.length > 0) {
|
||||
@@ -174,7 +236,20 @@ export default class EntityDataManager<TFields> {
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
await this.entityCache.invalidateManyAsync(fieldName, fieldValues);
|
||||
const dataLoader = this.getFieldDataLoaderForFieldName(fieldName);
|
||||
const dataLoader = this.getRegularFieldDataLoderForFieldName(fieldName);
|
||||
fieldValues.forEach((fieldValue) => dataLoader.clear(fieldValue));
|
||||
}
|
||||
|
||||
private async invalidateManyByFieldEqualingTransactionalAsync<N extends keyof TFields>(
|
||||
queryContext: EntityQueryContext,
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
if (!queryContext.isInTransaction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataLoader = this.getTransactionalFieldDataLoaderForFieldName(queryContext, fieldName);
|
||||
fieldValues.forEach((fieldValue) => dataLoader.clear(fieldValue));
|
||||
}
|
||||
|
||||
@@ -197,4 +272,21 @@ export default class EntityDataManager<TFields> {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async invalidateObjectFieldsTransactionalAsync(
|
||||
queryContext: EntityQueryContext,
|
||||
objectFields: Readonly<TFields>
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(objectFields) as (keyof TFields)[];
|
||||
await Promise.all(
|
||||
keys.map(async (fieldName: keyof TFields) => {
|
||||
const value = objectFields[fieldName];
|
||||
if (value !== undefined) {
|
||||
await this.invalidateManyByFieldEqualingTransactionalAsync(queryContext, fieldName, [
|
||||
value as NonNullable<TFields[keyof TFields]>,
|
||||
]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
spy,
|
||||
anyString,
|
||||
resetCalls,
|
||||
deepEqual,
|
||||
} from 'ts-mockito';
|
||||
|
||||
import EntityDatabaseAdapter from '../../EntityDatabaseAdapter';
|
||||
@@ -35,6 +36,7 @@ const getObjects = (): Map<string, TestFields[]> =>
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: '2',
|
||||
@@ -42,6 +44,7 @@ const getObjects = (): Map<string, TestFields[]> =>
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: '3',
|
||||
@@ -49,6 +52,7 @@ const getObjects = (): Map<string, TestFields[]> =>
|
||||
stringField: 'world',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -514,9 +518,30 @@ describe(EntityDataManager, () => {
|
||||
)
|
||||
).once();
|
||||
|
||||
verify(metricsAdapterMock.incrementDataManagerDataloaderLoadCount(1)).once();
|
||||
verify(metricsAdapterMock.incrementDataManagerCacheLoadCount(1)).once();
|
||||
verify(metricsAdapterMock.incrementDataManagerDatabaseLoadCount(1)).once();
|
||||
verify(
|
||||
metricsAdapterMock.incrementDataManagerDataloaderLoadCount(
|
||||
deepEqual({
|
||||
fieldValueCount: 1,
|
||||
entityClassName: TestEntity.name,
|
||||
})
|
||||
)
|
||||
).once();
|
||||
verify(
|
||||
metricsAdapterMock.incrementDataManagerCacheLoadCount(
|
||||
deepEqual({
|
||||
fieldValueCount: 1,
|
||||
entityClassName: TestEntity.name,
|
||||
})
|
||||
)
|
||||
).once();
|
||||
verify(
|
||||
metricsAdapterMock.incrementDataManagerDatabaseLoadCount(
|
||||
deepEqual({
|
||||
fieldValueCount: 1,
|
||||
entityClassName: TestEntity.name,
|
||||
})
|
||||
)
|
||||
).once();
|
||||
|
||||
resetCalls(metricsAdapterMock);
|
||||
|
||||
@@ -544,4 +569,34 @@ describe(EntityDataManager, () => {
|
||||
verify(metricsAdapterMock.incrementDataManagerCacheLoadCount(anyNumber())).never();
|
||||
verify(metricsAdapterMock.incrementDataManagerDatabaseLoadCount(anyNumber())).never();
|
||||
});
|
||||
|
||||
it('throws when a load-by value is null or undefined', async () => {
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
const entityDataManager = new EntityDataManager(
|
||||
databaseAdapter,
|
||||
entityCache,
|
||||
StubQueryContextProvider,
|
||||
new NoOpEntityMetricsAdapter(),
|
||||
TestEntity.name
|
||||
);
|
||||
const queryContext = StubQueryContextProvider.getQueryContext();
|
||||
|
||||
await expect(
|
||||
entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'nullableField', [null as any])
|
||||
).rejects.toThrowError('Invalid load: TestEntity (nullableField = null)');
|
||||
|
||||
await expect(
|
||||
entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'nullableField', [
|
||||
undefined as any,
|
||||
])
|
||||
).rejects.toThrowError('Invalid load: TestEntity (nullableField = undefined)');
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,11 @@ export interface EntityMetricsMutationEvent {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface IncrementLoadCountEvent {
|
||||
fieldValueCount: number;
|
||||
entityClassName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for gathering metrics about the Entity framework. Information about
|
||||
* entity load and mutation operations is piped to an instance of this adapter.
|
||||
@@ -45,7 +50,7 @@ export default interface IEntityMetricsAdapter {
|
||||
* load methods (not equality conjunction or raw).
|
||||
* @param fieldValueCount - count of field values being loaded for a field
|
||||
*/
|
||||
incrementDataManagerDataloaderLoadCount(fieldValueCount: number): void;
|
||||
incrementDataManagerDataloaderLoadCount(incrementLoadCountEvent: IncrementLoadCountEvent): void;
|
||||
|
||||
/**
|
||||
* Called when a cache load is initiated via the standard
|
||||
@@ -53,7 +58,7 @@ export default interface IEntityMetricsAdapter {
|
||||
* miss.
|
||||
* @param fieldValueCount - count of field values being loaded for a field
|
||||
*/
|
||||
incrementDataManagerCacheLoadCount(fieldValueCount: number): void;
|
||||
incrementDataManagerCacheLoadCount(incrementLoadCountEvent: IncrementLoadCountEvent): void;
|
||||
|
||||
/**
|
||||
* Called when a database load is initiated via the standard
|
||||
@@ -61,5 +66,5 @@ export default interface IEntityMetricsAdapter {
|
||||
* miss or when fetching an uncacheable field.
|
||||
* @param fieldValueCount - count of field values being loaded for a field
|
||||
*/
|
||||
incrementDataManagerDatabaseLoadCount(fieldValueCount: number): void;
|
||||
incrementDataManagerDatabaseLoadCount(incrementLoadCountEvent: IncrementLoadCountEvent): void;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import IEntityMetricsAdapter, {
|
||||
EntityMetricsLoadEvent,
|
||||
EntityMetricsMutationEvent,
|
||||
IncrementLoadCountEvent,
|
||||
} from './IEntityMetricsAdapter';
|
||||
|
||||
export default class NoOpEntityMetricsAdapter implements IEntityMetricsAdapter {
|
||||
logDataManagerLoadEvent(_loadEvent: EntityMetricsLoadEvent): void {}
|
||||
logMutatorMutationEvent(_mutationEvent: EntityMetricsMutationEvent): void {}
|
||||
incrementDataManagerDataloaderLoadCount(_fieldValueCount: number): void {}
|
||||
incrementDataManagerCacheLoadCount(_fieldValueCount: number): void {}
|
||||
incrementDataManagerDatabaseLoadCount(_fieldValueCount: number): void {}
|
||||
incrementDataManagerDataloaderLoadCount(
|
||||
_incrementLoadCountEvent: IncrementLoadCountEvent
|
||||
): void {}
|
||||
incrementDataManagerCacheLoadCount(_incrementLoadCountEvent: IncrementLoadCountEvent): void {}
|
||||
incrementDataManagerDatabaseLoadCount(_incrementLoadCountEvent: IncrementLoadCountEvent): void {}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export type TestFields = {
|
||||
stringField: string;
|
||||
numberField: number;
|
||||
dateField: Date;
|
||||
nullableField: string | null;
|
||||
};
|
||||
|
||||
export const testEntityConfiguration = new EntityConfiguration<TestFields>({
|
||||
@@ -40,6 +41,9 @@ export const testEntityConfiguration = new EntityConfiguration<TestFields>({
|
||||
dateField: new DateField({
|
||||
columnName: 'date_field',
|
||||
}),
|
||||
nullableField: new StringField({
|
||||
columnName: 'nullable_field',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
@@ -88,6 +92,7 @@ export default class TestEntity extends Entity<TestFields, string, ViewerContext
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -42,6 +43,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'wat',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -73,6 +75,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -80,6 +83,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -87,6 +91,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -138,6 +143,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -145,6 +151,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
@@ -152,6 +159,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -236,6 +244,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -269,6 +278,7 @@ describe(StubDatabaseAdapter, () => {
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
nullableField: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
@@ -559,17 +559,17 @@
|
||||
which "^1.3.1"
|
||||
|
||||
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
|
||||
version "0.6.0"
|
||||
version "0.10.0"
|
||||
dependencies:
|
||||
ioredis "^4.17.3"
|
||||
|
||||
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
|
||||
version "0.6.0"
|
||||
version "0.10.0"
|
||||
dependencies:
|
||||
knex "^0.21.5"
|
||||
|
||||
"@expo/entity@file:packages/entity":
|
||||
version "0.6.0"
|
||||
version "0.10.0"
|
||||
dependencies:
|
||||
"@expo/results" "^1.0.0"
|
||||
dataloader "^2.0.0"
|
||||
|
||||
Reference in new issue
Block a user