mirror of
https://github.com/expo/entity.git
synced 2026-11-15 13:18:13 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c92d10b80 | ||
|
|
7c180e84d9 | ||
|
|
1aa44e168e | ||
|
|
e91cfba993 | ||
|
|
252d945917 | ||
|
|
4e40b2e521 | ||
|
|
ccbdbc472a | ||
|
|
89cfb3d5a0 | ||
|
|
35db7e9ae8 | ||
|
|
60cc0fbda4 | ||
|
|
0445d3ea31 | ||
|
|
d25c1e142d |
No files matched your search
@@ -8,8 +8,9 @@
|
||||
"eslint-plugin-tsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": ["error",{ "paths": [".", ".."] }],
|
||||
"tsdoc/syntax": "warn"
|
||||
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
|
||||
"tsdoc/syntax": "warn",
|
||||
"no-console": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -10,6 +10,8 @@ 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
|
||||
|
||||
+45
-4
@@ -1,6 +1,47 @@
|
||||
# Changelog
|
||||
# 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.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix stub cache key generation for global cache ([#59](https://github.com/expo/entity/issues/59)) ([7c180e8](https://github.com/expo/entity/commit/7c180e84d9a8cdfd9380fac0eab2d02a96ec18f3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct some typing issues and exports ([#57](https://github.com/expo/entity/issues/57)) ([e91cfba](https://github.com/expo/entity/commit/e91cfba99321c2c4da2078aafd8627cbf0ceca50))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
## v0.4.0 (2020-06-03)
|
||||
|
||||
* All Packages
|
||||
* Documentation Improvements
|
||||
@@ -16,11 +57,11 @@
|
||||
* `@expo/entity-example`
|
||||
* Initial Version
|
||||
|
||||
## v0.3.0 (2020-04-28)
|
||||
# 0.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
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.4.0"
|
||||
"version": "0.5.2"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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.4.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Redis cache adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -12,7 +12,7 @@
|
||||
"tsc": "tsc",
|
||||
"clean": "rm -rf build coverage coverage-integration",
|
||||
"lint": "eslint src --ext '.ts'",
|
||||
"test": "jest --config ../../resources/jest.config.js --rootDir",
|
||||
"test": "jest --rootDir --config ../../resources/jest.config.js",
|
||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
|
||||
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
|
||||
@@ -32,6 +32,6 @@
|
||||
"ioredis": "^4.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.4.0"
|
||||
"@expo/entity": "^0.5.2"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -44,7 +44,7 @@ describe(RedisCacheAdapter, () => {
|
||||
const cacheAdapter = vc1.entityCompanionProvider.getCompanionForEntity(
|
||||
RedisTestEntity,
|
||||
RedisTestEntity.getCompanionDefinition()
|
||||
)['cacheAdapter'];
|
||||
)['tableDataCoordinator']['cacheAdapter'];
|
||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
||||
|
||||
// creating an entity should put it in cache (loader loads it after creation)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
||||
import {
|
||||
CacheStatus,
|
||||
UUIDField,
|
||||
EntityConfiguration,
|
||||
DatabaseAdapterFlavor,
|
||||
CacheAdapterFlavor,
|
||||
} from '@expo/entity';
|
||||
import { Redis, Pipeline } from 'ioredis';
|
||||
import { mock, when, instance, anything, verify } from 'ts-mockito';
|
||||
|
||||
@@ -14,6 +20,8 @@ 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 = {
|
||||
const redisTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: RedisTestEntity,
|
||||
entityConfiguration: redisTestEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: RedisTestEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
+2
-2
@@ -4,8 +4,8 @@ import {
|
||||
EntityCompanionProvider,
|
||||
CacheAdapterFlavor,
|
||||
DatabaseAdapterFlavor,
|
||||
StubDatabaseAdapter,
|
||||
StubQueryContextProvider,
|
||||
StubDatabaseAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
|
||||
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
|
||||
@@ -19,7 +19,7 @@ export const createRedisIntegrationTestEntityCompanionProvider = (
|
||||
metricsAdapter,
|
||||
{
|
||||
[DatabaseAdapterFlavor.POSTGRES]: {
|
||||
adapter: StubDatabaseAdapter,
|
||||
adapterProvider: new StubDatabaseAdapterProvider(),
|
||||
queryContextProvider: StubQueryContextProvider,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct some typing issues and exports ([#57](https://github.com/expo/entity/issues/57)) ([e91cfba](https://github.com/expo/entity/commit/e91cfba99321c2c4da2078aafd8627cbf0ceca50))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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.4.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Knex database adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -12,7 +12,7 @@
|
||||
"tsc": "tsc",
|
||||
"clean": "rm -rf build coverage coverage-integration",
|
||||
"lint": "eslint src --ext '.ts'",
|
||||
"test": "jest --config ../../resources/jest.config.js --rootDir --passWithNoTests",
|
||||
"test": "jest --rootDir --config ../../resources/jest.config.js --passWithNoTests",
|
||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
|
||||
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
|
||||
@@ -32,6 +32,6 @@
|
||||
"knex": "^0.20.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.4.0"
|
||||
"@expo/entity": "^0.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
IEntityDatabaseAdapterProvider,
|
||||
EntityConfiguration,
|
||||
EntityDatabaseAdapter,
|
||||
} from '@expo/entity';
|
||||
|
||||
import PostgresEntityDatabaseAdapter from './PostgresEntityDatabaseAdapter';
|
||||
|
||||
export default class PostgresEntityDatabaseAdapterProvider
|
||||
implements IEntityDatabaseAdapterProvider {
|
||||
getDatabaseAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityDatabaseAdapter<TFields> {
|
||||
return new PostgresEntityDatabaseAdapter(entityConfiguration);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as PostgresEntityDatabaseAdapter } from './PostgresEntityDatabaseAdapter';
|
||||
export { default as PostgresEntityDatabaseAdapterProvider } from './PostgresEntityDatabaseAdapterProvider';
|
||||
export { default as PostgresEntityQueryContextProvider } from './PostgresEntityQueryContextProvider';
|
||||
@@ -104,12 +104,12 @@ export const invalidTestEntityConfiguration = new EntityConfiguration<InvalidTes
|
||||
columnName: 'name',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
const invalidTestEntityCompanionDefinition = {
|
||||
const invalidTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
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 = {
|
||||
const postgresTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: PostgresTestEntity,
|
||||
entityConfiguration: postgresTestEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: PostgresTestEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
+2
-2
@@ -8,7 +8,7 @@ import {
|
||||
} from '@expo/entity';
|
||||
import Knex from 'knex';
|
||||
|
||||
import PostgresEntityDatabaseAdapter from '../PostgresEntityDatabaseAdapter';
|
||||
import PostgresEntityDatabaseAdapterProvider from '../PostgresEntityDatabaseAdapterProvider';
|
||||
import PostgresEntityQueryContextProvider from '../PostgresEntityQueryContextProvider';
|
||||
|
||||
export const createKnexIntegrationTestEntityCompanionProvider = (
|
||||
@@ -19,7 +19,7 @@ export const createKnexIntegrationTestEntityCompanionProvider = (
|
||||
metricsAdapter,
|
||||
{
|
||||
[DatabaseAdapterFlavor.POSTGRES]: {
|
||||
adapter: PostgresEntityDatabaseAdapter,
|
||||
adapterProvider: new PostgresEntityDatabaseAdapterProvider(),
|
||||
queryContextProvider: new PostgresEntityQueryContextProvider(knex),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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))
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@expo/entity-example",
|
||||
"private": true,
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"description": "An example integration of the @expo/entity framework",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
"clean": "rm -rf build coverage coverage-integration",
|
||||
"lint": "eslint src --ext '.ts'",
|
||||
"test": "jest --config ../../resources/jest.config.js --rootDir",
|
||||
"test": "jest --rootDir --config ../../resources/jest.config.js",
|
||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
|
||||
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete",
|
||||
@@ -22,8 +22,8 @@
|
||||
"author": "Expo",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/entity": "^0.4.0",
|
||||
"apollo-server-koa": "^2.13.0",
|
||||
"@expo/entity": "^0.5.2",
|
||||
"apollo-server-koa": "^2.14.2",
|
||||
"graphql": "^15.0.0",
|
||||
"koa": "^2.11.0",
|
||||
"koa-body": "^4.1.1",
|
||||
|
||||
@@ -7,17 +7,26 @@ import {
|
||||
TableQuerySelectionModifiers,
|
||||
getDatabaseFieldForEntityField,
|
||||
EntityConfiguration,
|
||||
IEntityDatabaseAdapterProvider,
|
||||
} from '@expo/entity';
|
||||
import invariant from 'invariant';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const dbObjects: Readonly<{ [key: string]: any }>[] = [];
|
||||
|
||||
export class InMemoryDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
|
||||
getDatabaseAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityDatabaseAdapter<TFields> {
|
||||
return new InMemoryDatabaseAdapter(entityConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory database adapter for entity for the purposes of this example. Normally `@expo/entity-database-adapter-knex`
|
||||
* or another production adapter would be used. Very similar to StubDatabaseAdapter but shared in a way more akin to a normal database.
|
||||
*/
|
||||
export default class InMemoryDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
class InMemoryDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
protected getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DatabaseAdapterFlavor,
|
||||
} from '@expo/entity';
|
||||
|
||||
import InMemoryDatabaseAdapter from './adapters/InMemoryDatabaseAdapter';
|
||||
import { InMemoryDatabaseAdapterProvider } from './adapters/InMemoryDatabaseAdapter';
|
||||
import InMemoryQueryContextProvider from './adapters/InMemoryQueryContextProvider';
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,7 @@ export const createEntityCompanionProvider = (
|
||||
// An in-memory DB is used for demonstration purposes, but generally this would be
|
||||
// instantiated with PostgresEntityDatabaseAdapter and PostgresEntityQueryContextProvider
|
||||
[DatabaseAdapterFlavor.POSTGRES]: {
|
||||
adapter: InMemoryDatabaseAdapter,
|
||||
adapterProvider: new InMemoryDatabaseAdapterProvider(),
|
||||
queryContextProvider: new InMemoryQueryContextProvider(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 = {
|
||||
export const noteEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: NoteEntity,
|
||||
entityConfiguration: new EntityConfiguration<NoteFields>({
|
||||
idField: 'id',
|
||||
@@ -57,8 +57,8 @@ export const noteEntityCompanion = {
|
||||
columnName: 'body',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
}),
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: NotePrivacyPolicy,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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.2](https://github.com/expo/entity/compare/v0.5.1...v0.5.2) (2020-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix stub cache key generation for global cache ([#59](https://github.com/expo/entity/issues/59)) ([7c180e8](https://github.com/expo/entity/commit/7c180e84d9a8cdfd9380fac0eab2d02a96ec18f3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/expo/entity/compare/v0.5.0...v0.5.1) (2020-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct some typing issues and exports ([#57](https://github.com/expo/entity/issues/57)) ([e91cfba](https://github.com/expo/entity/commit/e91cfba99321c2c4da2078aafd8627cbf0ceca50))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"description": "A privacy-first data model",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -12,7 +12,7 @@
|
||||
"tsc": "tsc",
|
||||
"clean": "rm -rf build coverage coverage-integration",
|
||||
"lint": "eslint src --ext '.ts'",
|
||||
"test": "jest --config ../../resources/jest.config.js --rootDir",
|
||||
"test": "jest --rootDir --config ../../resources/jest.config.js",
|
||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
|
||||
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
|
||||
|
||||
@@ -13,8 +13,15 @@ export default class EnforcingEntityLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityLoader: EntityLoader<
|
||||
@@ -22,7 +29,8 @@ export default class EnforcingEntityLoader<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>
|
||||
) {}
|
||||
|
||||
|
||||
+118
-25
@@ -28,8 +28,9 @@ import ViewerContext from './ViewerContext';
|
||||
export default abstract class Entity<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext
|
||||
> extends ReadonlyEntity<TFields, TID, TViewerContext> {
|
||||
TViewerContext extends ViewerContext,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields> {
|
||||
/**
|
||||
* Vend mutator for creating a new entity in given query context.
|
||||
* @param viewerContext - viewer context of creating user
|
||||
@@ -41,16 +42,30 @@ export default abstract class Entity<
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2,
|
||||
queryContext: EntityQueryContext = viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getQueryContextProvider()
|
||||
.getRegularEntityQueryContext()
|
||||
): CreateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
|
||||
): CreateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
|
||||
return viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getMutatorFactory()
|
||||
@@ -67,17 +82,31 @@ export default abstract class Entity<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
existingEntity: TMEntity,
|
||||
queryContext: EntityQueryContext = existingEntity
|
||||
.getViewerContext()
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getQueryContextProvider()
|
||||
.getRegularEntityQueryContext()
|
||||
): UpdateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
|
||||
): UpdateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
|
||||
return existingEntity
|
||||
.getViewerContext()
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
@@ -94,10 +123,24 @@ export default abstract class Entity<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
existingEntity: TMEntity,
|
||||
queryContext: EntityQueryContext = existingEntity
|
||||
.getViewerContext()
|
||||
@@ -122,10 +165,24 @@ export default abstract class Entity<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
existingEntity: TMEntity,
|
||||
queryContext: EntityQueryContext = existingEntity
|
||||
.getViewerContext()
|
||||
@@ -161,10 +218,24 @@ export default abstract class Entity<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
existingEntity: TMEntity,
|
||||
queryContext: EntityQueryContext = existingEntity
|
||||
.getViewerContext()
|
||||
@@ -196,10 +267,24 @@ export default abstract class Entity<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends Entity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
existingEntity: TMEntity,
|
||||
queryContext: EntityQueryContext = existingEntity
|
||||
.getViewerContext()
|
||||
@@ -226,8 +311,15 @@ export interface IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
new (viewerContext: TViewerContext, obj: Readonly<TFields>): TEntity;
|
||||
getCompanionDefinition(): EntityCompanionDefinition<
|
||||
@@ -235,6 +327,7 @@ export interface IEntityClass<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>;
|
||||
}
|
||||
@@ -10,7 +10,8 @@ export default class EntityAssociationLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(private readonly entity: TEntity) {}
|
||||
|
||||
@@ -24,21 +25,29 @@ export default class EntityAssociationLoader<
|
||||
async loadAssociatedEntityAsync<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
|
||||
TAssociatedEntity extends ReadonlyEntity<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity
|
||||
>
|
||||
TAssociatedEntity,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
>(
|
||||
fieldIdentifyingAssociatedEntity: keyof TFields,
|
||||
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
|
||||
associatedEntityClass: IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedPrivacyPolicy
|
||||
TAssociatedPrivacyPolicy,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
queryContext: EntityQueryContext = this.entity
|
||||
.getViewerContext()
|
||||
@@ -55,6 +64,60 @@ 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,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
>(
|
||||
associatedEntityClass: IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedPrivacyPolicy,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
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.
|
||||
@@ -66,21 +129,29 @@ export default class EntityAssociationLoader<
|
||||
async loadAssociatedEntityByFieldEqualingAsync<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
|
||||
TAssociatedEntity extends ReadonlyEntity<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity
|
||||
>
|
||||
TAssociatedEntity,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
>(
|
||||
fieldIdentifyingAssociatedEntity: keyof TFields,
|
||||
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
|
||||
associatedEntityClass: IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedPrivacyPolicy
|
||||
TAssociatedPrivacyPolicy,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
associatedEntityLookupByField: keyof TAssociatedFields,
|
||||
queryContext: EntityQueryContext = this.entity
|
||||
@@ -101,6 +172,60 @@ 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,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
>(
|
||||
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>,
|
||||
associatedEntityClass: IEntityClass<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedPrivacyPolicy,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
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.
|
||||
@@ -108,15 +233,29 @@ 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>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>
|
||||
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<
|
||||
TFields2,
|
||||
TID2,
|
||||
TViewerContext,
|
||||
TEntity2,
|
||||
TSelectedFields2
|
||||
>,
|
||||
TSelectedFields2 extends keyof TFields2 = keyof TFields2
|
||||
>(
|
||||
loadDirectives: [
|
||||
EntityLoadThroughDirective<TViewerContext, TFields, TFields2, TID2, TEntity2, TPrivacyPolicy2>
|
||||
EntityLoadThroughDirective<
|
||||
TViewerContext,
|
||||
TFields,
|
||||
TFields2,
|
||||
TID2,
|
||||
TEntity2,
|
||||
TPrivacyPolicy2,
|
||||
TSelectedFields,
|
||||
TSelectedFields2
|
||||
>
|
||||
],
|
||||
queryContext?: EntityQueryContext
|
||||
): Promise<Result<TEntity2> | null>;
|
||||
@@ -128,16 +267,28 @@ 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>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>,
|
||||
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<
|
||||
TFields2,
|
||||
TID2,
|
||||
TViewerContext,
|
||||
TEntity2,
|
||||
TSelectedFields2
|
||||
>,
|
||||
TFields3,
|
||||
TID3,
|
||||
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext>,
|
||||
TPrivacyPolicy3 extends EntityPrivacyPolicy<TFields3, TID3, TViewerContext, TEntity3>
|
||||
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
|
||||
>(
|
||||
loadDirectives: [
|
||||
EntityLoadThroughDirective<
|
||||
@@ -146,7 +297,9 @@ export default class EntityAssociationLoader<
|
||||
TFields2,
|
||||
TID2,
|
||||
TEntity2,
|
||||
TPrivacyPolicy2
|
||||
TPrivacyPolicy2,
|
||||
TSelectedFields,
|
||||
TSelectedFields2
|
||||
>,
|
||||
EntityLoadThroughDirective<
|
||||
TViewerContext,
|
||||
@@ -154,7 +307,9 @@ export default class EntityAssociationLoader<
|
||||
TFields3,
|
||||
TID3,
|
||||
TEntity3,
|
||||
TPrivacyPolicy3
|
||||
TPrivacyPolicy3,
|
||||
TSelectedFields2,
|
||||
TSelectedFields3
|
||||
>
|
||||
],
|
||||
queryContext?: EntityQueryContext
|
||||
@@ -167,20 +322,39 @@ 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>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TID2, TViewerContext, TEntity2>,
|
||||
TEntity2 extends ReadonlyEntity<TFields2, TID2, TViewerContext, TSelectedFields2>,
|
||||
TPrivacyPolicy2 extends EntityPrivacyPolicy<
|
||||
TFields2,
|
||||
TID2,
|
||||
TViewerContext,
|
||||
TEntity2,
|
||||
TSelectedFields2
|
||||
>,
|
||||
TFields3,
|
||||
TID3,
|
||||
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext>,
|
||||
TPrivacyPolicy3 extends EntityPrivacyPolicy<TFields3, TID3, TViewerContext, TEntity3>,
|
||||
TEntity3 extends ReadonlyEntity<TFields3, TID3, TViewerContext, TSelectedFields3>,
|
||||
TPrivacyPolicy3 extends EntityPrivacyPolicy<
|
||||
TFields3,
|
||||
TID3,
|
||||
TViewerContext,
|
||||
TEntity3,
|
||||
TSelectedFields3
|
||||
>,
|
||||
TFields4,
|
||||
TID4,
|
||||
TEntity4 extends ReadonlyEntity<TFields4, TID4, TViewerContext>,
|
||||
TPrivacyPolicy4 extends EntityPrivacyPolicy<TFields4, TID4, TViewerContext, TEntity4>
|
||||
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
|
||||
>(
|
||||
loadDirective: [
|
||||
EntityLoadThroughDirective<
|
||||
@@ -189,7 +363,9 @@ export default class EntityAssociationLoader<
|
||||
TFields2,
|
||||
TID2,
|
||||
TEntity2,
|
||||
TPrivacyPolicy2
|
||||
TPrivacyPolicy2,
|
||||
TSelectedFields,
|
||||
TSelectedFields2
|
||||
>,
|
||||
EntityLoadThroughDirective<
|
||||
TViewerContext,
|
||||
@@ -197,7 +373,9 @@ export default class EntityAssociationLoader<
|
||||
TFields3,
|
||||
TID3,
|
||||
TEntity3,
|
||||
TPrivacyPolicy3
|
||||
TPrivacyPolicy3,
|
||||
TSelectedFields2,
|
||||
TSelectedFields3
|
||||
>,
|
||||
EntityLoadThroughDirective<
|
||||
TViewerContext,
|
||||
@@ -205,7 +383,9 @@ export default class EntityAssociationLoader<
|
||||
TFields4,
|
||||
TID4,
|
||||
TEntity4,
|
||||
TPrivacyPolicy4
|
||||
TPrivacyPolicy4,
|
||||
TSelectedFields3,
|
||||
TSelectedFields4
|
||||
>
|
||||
],
|
||||
queryContext?: EntityQueryContext
|
||||
@@ -218,22 +398,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>[],
|
||||
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
|
||||
queryContext?: EntityQueryContext
|
||||
): Promise<Result<ReadonlyEntity<any, any, any>> | null>;
|
||||
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null>;
|
||||
|
||||
async loadAssociatedEntityThroughAsync(
|
||||
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any>[],
|
||||
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
|
||||
queryContext?: EntityQueryContext
|
||||
): Promise<Result<ReadonlyEntity<any, any, any>> | null> {
|
||||
let currentEntity: ReadonlyEntity<any, any, any> = this.entity;
|
||||
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null> {
|
||||
let currentEntity: ReadonlyEntity<any, any, any, any> = this.entity;
|
||||
for (const loadDirective of loadDirectives) {
|
||||
const {
|
||||
associatedEntityClass,
|
||||
fieldIdentifyingAssociatedEntity,
|
||||
associatedEntityLookupByField,
|
||||
} = loadDirective;
|
||||
let associatedEntityResult: Result<ReadonlyEntity<any, any, any>> | null;
|
||||
let associatedEntityResult: Result<ReadonlyEntity<any, any, any, any>> | null;
|
||||
if (associatedEntityLookupByField) {
|
||||
associatedEntityResult = await currentEntity
|
||||
.associationLoader()
|
||||
@@ -274,13 +454,21 @@ export interface EntityLoadThroughDirective<
|
||||
TFields,
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
|
||||
TAssociatedEntity extends ReadonlyEntity<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TAssociatedFields,
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity
|
||||
>
|
||||
TAssociatedEntity,
|
||||
TAssociatedSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields,
|
||||
TAssociatedSelectedFields extends keyof TAssociatedFields = keyof TAssociatedFields
|
||||
> {
|
||||
/**
|
||||
* Class of entity to load at this step.
|
||||
@@ -290,13 +478,14 @@ export interface EntityLoadThroughDirective<
|
||||
TAssociatedID,
|
||||
TViewerContext,
|
||||
TAssociatedEntity,
|
||||
TAssociatedPrivacyPolicy
|
||||
TAssociatedPrivacyPolicy,
|
||||
TAssociatedSelectedFields
|
||||
>;
|
||||
|
||||
/**
|
||||
* Field of the current entity with which to load an instance of associatedEntityClass.
|
||||
*/
|
||||
fieldIdentifyingAssociatedEntity: keyof TFields;
|
||||
fieldIdentifyingAssociatedEntity: keyof Pick<TFields, TSelectedFields>;
|
||||
|
||||
/**
|
||||
* Field by which to load the instance of associatedEntityClass. If not provided, the
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
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 IEntityQueryContextProvider from './IEntityQueryContextProvider';
|
||||
import ReadonlyEntity from './ReadonlyEntity';
|
||||
import ViewerContext from './ViewerContext';
|
||||
import EntityDataManager from './internal/EntityDataManager';
|
||||
import ReadThroughEntityCache from './internal/ReadThroughEntityCache';
|
||||
import EntityTableDataCoordinator from './internal/EntityTableDataCoordinator';
|
||||
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
|
||||
|
||||
export interface IDatabaseAdapterClass<TFields> {
|
||||
new (entityConfiguration: EntityConfiguration<TFields>): EntityDatabaseAdapter<TFields>;
|
||||
}
|
||||
|
||||
export interface IPrivacyPolicyClass<TPrivacyPolicy> {
|
||||
new (): TPrivacyPolicy;
|
||||
}
|
||||
@@ -28,72 +19,89 @@ export default class EntityCompanion<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
// 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
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>;
|
||||
private readonly entityMutatorFactory: EntityMutatorFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>;
|
||||
|
||||
constructor(
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
DatabaseAdapterClass: IDatabaseAdapterClass<TFields>,
|
||||
cacheAdapterProvider: IEntityCacheAdapterProvider,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly tableDataCoordinator: EntityTableDataCoordinator<TFields>,
|
||||
PrivacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>,
|
||||
private readonly queryContextProvider: IEntityQueryContextProvider,
|
||||
|
||||
metricsAdapter: IEntityMetricsAdapter
|
||||
) {
|
||||
this.databaseAdapter = new DatabaseAdapterClass(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(
|
||||
entityConfiguration,
|
||||
tableDataCoordinator.entityConfiguration.idField,
|
||||
entityClass,
|
||||
privacyPolicy,
|
||||
dataManager
|
||||
tableDataCoordinator.dataManager
|
||||
);
|
||||
this.entityMutatorFactory = new EntityMutatorFactory(
|
||||
entityConfiguration,
|
||||
tableDataCoordinator.entityConfiguration.idField,
|
||||
entityClass,
|
||||
privacyPolicy,
|
||||
this.entityLoaderFactory,
|
||||
this.databaseAdapter,
|
||||
tableDataCoordinator.databaseAdapter,
|
||||
metricsAdapter
|
||||
);
|
||||
}
|
||||
|
||||
getLoaderFactory(): EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
getLoaderFactory(): EntityLoaderFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return this.entityLoaderFactory;
|
||||
}
|
||||
|
||||
getMutatorFactory(): EntityMutatorFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
getMutatorFactory(): EntityMutatorFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return this.entityMutatorFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query context provider for this entity.
|
||||
*/
|
||||
getQueryContextProvider(): IEntityQueryContextProvider {
|
||||
return this.queryContextProvider;
|
||||
return this.tableDataCoordinator.getQueryContextProvider();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IEntityClass } from './Entity';
|
||||
import EntityCompanion, { IDatabaseAdapterClass, IPrivacyPolicyClass } from './EntityCompanion';
|
||||
import EntityCompanion, { IPrivacyPolicyClass } from './EntityCompanion';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
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 IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
|
||||
import { computeIfAbsent } from './utils/collections/maps';
|
||||
|
||||
@@ -37,8 +39,8 @@ export enum CacheAdapterFlavor {
|
||||
* will use the specified adapter for database accesses and the specified query context provider
|
||||
* for providing query contexts.
|
||||
*/
|
||||
export interface DatabaseAdapterFlavorDefinition<TFields> {
|
||||
adapter: IDatabaseAdapterClass<TFields>;
|
||||
export interface DatabaseAdapterFlavorDefinition {
|
||||
adapterProvider: IEntityDatabaseAdapterProvider;
|
||||
queryContextProvider: IEntityQueryContextProvider;
|
||||
}
|
||||
|
||||
@@ -54,18 +56,55 @@ 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 interface EntityCompanionDefinition<
|
||||
export class EntityCompanionDefinition<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>;
|
||||
entityConfiguration: EntityConfiguration<TFields>;
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor;
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor;
|
||||
privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,9 +117,13 @@ export interface EntityCompanionDefinition<
|
||||
* {@link EntityCompanion} for each type of {@link Entity}.
|
||||
*/
|
||||
export default class EntityCompanionProvider {
|
||||
private readonly entityCompanionMap: Map<
|
||||
private readonly companionMap: Map<
|
||||
string,
|
||||
EntityCompanion<any, any, any, any, any>
|
||||
EntityCompanion<any, any, any, any, any, any>
|
||||
> = new Map();
|
||||
private readonly tableDataCoordinatorMap: Map<
|
||||
string,
|
||||
EntityTableDataCoordinator<any>
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
@@ -91,10 +134,7 @@ export default class EntityCompanionProvider {
|
||||
*/
|
||||
constructor(
|
||||
private metricsAdapter: IEntityMetricsAdapter,
|
||||
private databaseAdapterFlavors: Record<
|
||||
DatabaseAdapterFlavor,
|
||||
DatabaseAdapterFlavorDefinition<any>
|
||||
>,
|
||||
private databaseAdapterFlavors: Record<DatabaseAdapterFlavor, DatabaseAdapterFlavorDefinition>,
|
||||
private cacheAdapterFlavors: Record<CacheAdapterFlavor, CacheAdapterFlavorDefinition>
|
||||
) {}
|
||||
|
||||
@@ -109,31 +149,60 @@ export default class EntityCompanionProvider {
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
>(
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
entityCompanionDefinition: EntityCompanionDefinition<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>
|
||||
): EntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
return computeIfAbsent(this.entityCompanionMap, entityClass.name, () => {
|
||||
const entityDatabaseAdapterFlavor = this.databaseAdapterFlavors[
|
||||
entityCompanionDefinition.databaseAdaptorFlavor
|
||||
];
|
||||
const entityCacheAdapterFlavor = this.cacheAdapterFlavors[
|
||||
entityCompanionDefinition.cacheAdaptorFlavor
|
||||
];
|
||||
): EntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
const tableDataCoordinator = this.getTableDataCoordinatorForEntity(
|
||||
entityCompanionDefinition.entityConfiguration
|
||||
);
|
||||
return computeIfAbsent(this.companionMap, entityClass.name, () => {
|
||||
return new EntityCompanion(
|
||||
entityCompanionDefinition.entityClass,
|
||||
entityCompanionDefinition.entityConfiguration,
|
||||
entityDatabaseAdapterFlavor.adapter,
|
||||
entityCacheAdapterFlavor.cacheAdapterProvider,
|
||||
tableDataCoordinator,
|
||||
entityCompanionDefinition.privacyPolicyClass,
|
||||
this.metricsAdapter
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getTableDataCoordinatorForEntity<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityTableDataCoordinator<TFields> {
|
||||
return computeIfAbsent(this.tableDataCoordinatorMap, entityConfiguration.tableName, () => {
|
||||
const entityDatabaseAdapterFlavor = this.databaseAdapterFlavors[
|
||||
entityConfiguration.databaseAdapterFlavor
|
||||
];
|
||||
const entityCacheAdapterFlavor = this.cacheAdapterFlavors[
|
||||
entityConfiguration.cacheAdapterFlavor
|
||||
];
|
||||
return new EntityTableDataCoordinator(
|
||||
entityConfiguration,
|
||||
entityDatabaseAdapterFlavor.adapterProvider,
|
||||
entityCacheAdapterFlavor.cacheAdapterProvider,
|
||||
entityDatabaseAdapterFlavor.queryContextProvider,
|
||||
this.metricsAdapter
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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, and field mappings.
|
||||
* cachable fields, field mappings, and types of cache and database adapter.
|
||||
*/
|
||||
export default class EntityConfiguration<TFields> {
|
||||
readonly idField: keyof TFields;
|
||||
@@ -15,20 +16,29 @@ export default class EntityConfiguration<TFields> {
|
||||
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>;
|
||||
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
|
||||
|
||||
@@ -11,12 +11,26 @@ export class EntityNotFoundError<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>,
|
||||
N extends keyof TFields
|
||||
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
|
||||
> extends EntityError {
|
||||
constructor(
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
fieldName: N,
|
||||
fieldValue: TFields[N]
|
||||
) {
|
||||
@@ -28,7 +42,8 @@ export class EntityNotAuthorizedError<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends EntityError {
|
||||
public readonly entityClassName: string;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ 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';
|
||||
@@ -21,22 +20,30 @@ export default class EntityLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly viewerContext: TViewerContext,
|
||||
private readonly queryContext: EntityQueryContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly idField: keyof TFields,
|
||||
private readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly privacyPolicy: TPrivacyPolicy,
|
||||
private readonly entityDataManager: EntityDataManager<TFields>
|
||||
private readonly dataManager: EntityDataManager<TFields>
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -44,7 +51,14 @@ 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> {
|
||||
enforcing(): EnforcingEntityLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new EnforcingEntityLoader(this);
|
||||
}
|
||||
|
||||
@@ -59,7 +73,7 @@ export default class EntityLoader<
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
|
||||
const fieldValuesToObjects = await this.entityDataManager.loadManyByFieldEqualingAsync(
|
||||
const fieldValuesToObjects = await this.dataManager.loadManyByFieldEqualingAsync(
|
||||
this.queryContext,
|
||||
fieldName,
|
||||
fieldValues
|
||||
@@ -128,9 +142,7 @@ 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.entityConfiguration.idField, id as any)
|
||||
);
|
||||
return result(new EntityNotFoundError(this.entityClass, this.idField, id as any));
|
||||
}
|
||||
return entityResult;
|
||||
}
|
||||
@@ -141,7 +153,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.entityConfiguration.idField, id as any);
|
||||
return await this.loadByFieldEqualingAsync(this.idField, id as any);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,16 +164,13 @@ export default class EntityLoader<
|
||||
*/
|
||||
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
|
||||
const entityResults = ((await this.loadManyByFieldEqualingManyAsync(
|
||||
this.entityConfiguration.idField,
|
||||
this.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.entityConfiguration.idField, id as any)
|
||||
)
|
||||
entityResult ?? result(new EntityNotFoundError(this.entityClass, this.idField, id as any))
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -184,7 +193,7 @@ export default class EntityLoader<
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const fieldValuesArray = await this.entityDataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
const fieldValuesArray = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers
|
||||
@@ -232,7 +241,7 @@ export default class EntityLoader<
|
||||
bindings: any[] | object,
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const fieldValuesArray = await this.entityDataManager.loadManyByRawWhereClauseAsync(
|
||||
const fieldValuesArray = await this.dataManager.loadManyByRawWhereClauseAsync(
|
||||
this.queryContext,
|
||||
rawWhereClause,
|
||||
bindings,
|
||||
@@ -256,6 +265,6 @@ export default class EntityLoader<
|
||||
* @param objectFields - entity data object to be invalidated
|
||||
*/
|
||||
async invalidateFieldsAsync(objectFields: Readonly<TFields>): Promise<void> {
|
||||
await this.entityDataManager.invalidateObjectFieldsAsync(objectFields);
|
||||
await this.dataManager.invalidateObjectFieldsAsync(objectFields);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IEntityClass } from './Entity';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import EntityLoader from './EntityLoader';
|
||||
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from './EntityQueryContext';
|
||||
@@ -14,17 +13,25 @@ export default class EntityLoaderFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly idField: keyof TFields,
|
||||
private readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly privacyPolicyClass: TPrivacyPolicy,
|
||||
private readonly dataManager: EntityDataManager<TFields>
|
||||
@@ -38,11 +45,11 @@ export default class EntityLoaderFactory<
|
||||
forLoad(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext
|
||||
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
this.entityConfiguration,
|
||||
this.idField,
|
||||
this.entityClass,
|
||||
this.privacyPolicyClass,
|
||||
this.dataManager
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Result, asyncResult, result, enforceAsyncResult } from '@expo/results';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Entity, { IEntityClass } from './Entity';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
|
||||
import EntityLoaderFactory from './EntityLoaderFactory';
|
||||
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
|
||||
@@ -15,19 +14,27 @@ abstract class BaseMutator<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends Entity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
protected readonly viewerContext: TViewerContext,
|
||||
protected readonly queryContext: EntityQueryContext,
|
||||
protected readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
protected readonly idField: keyof TFields,
|
||||
protected readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
protected readonly privacyPolicy: TPrivacyPolicy,
|
||||
protected readonly entityLoaderFactory: EntityLoaderFactory<
|
||||
@@ -35,7 +42,8 @@ abstract class BaseMutator<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
protected readonly databaseAdapter: EntityDatabaseAdapter<TFields>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter
|
||||
@@ -49,9 +57,16 @@ export class CreateMutator<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends Entity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
private readonly fieldsForEntity: Partial<TFields> = {};
|
||||
|
||||
/**
|
||||
@@ -85,7 +100,7 @@ export class CreateMutator<
|
||||
|
||||
private async createInternalAsync(): Promise<Result<TEntity>> {
|
||||
const temporaryEntityForPrivacyCheck = new this.entityClass(this.viewerContext, ({
|
||||
[this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
|
||||
[this.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
|
||||
...this.fieldsForEntity,
|
||||
} as unknown) as TFields);
|
||||
|
||||
@@ -120,9 +135,16 @@ export class UpdateMutator<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends Entity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
private readonly originalFieldsForEntity: Readonly<TFields>;
|
||||
private readonly fieldsForEntity: TFields;
|
||||
private readonly updatedFields: Partial<TFields> = {};
|
||||
@@ -130,10 +152,24 @@ export class UpdateMutator<
|
||||
constructor(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
idField: keyof TFields,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
privacyPolicy: TPrivacyPolicy,
|
||||
entityLoaderFactory: EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityLoaderFactory: EntityLoaderFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
databaseAdapter: EntityDatabaseAdapter<TFields>,
|
||||
metricsAdapter: IEntityMetricsAdapter,
|
||||
fieldsForEntity: Readonly<TFields>
|
||||
@@ -141,7 +177,7 @@ export class UpdateMutator<
|
||||
super(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
entityConfiguration,
|
||||
idField,
|
||||
entityClass,
|
||||
privacyPolicy,
|
||||
entityLoaderFactory,
|
||||
@@ -198,7 +234,7 @@ export class UpdateMutator<
|
||||
|
||||
const updateResult = await this.databaseAdapter.updateAsync(
|
||||
this.queryContext,
|
||||
this.entityConfiguration.idField,
|
||||
this.idField,
|
||||
entityAboutToBeUpdated.getID(),
|
||||
this.updatedFields
|
||||
);
|
||||
@@ -220,16 +256,37 @@ export class DeleteMutator<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends Entity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> extends BaseMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
constructor(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
idField: keyof TFields,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
privacyPolicy: TPrivacyPolicy,
|
||||
entityLoaderFactory: EntityLoaderFactory<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityLoaderFactory: EntityLoaderFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
databaseAdapter: EntityDatabaseAdapter<TFields>,
|
||||
metricsAdapter: IEntityMetricsAdapter,
|
||||
private readonly entity: TEntity
|
||||
@@ -237,7 +294,7 @@ export class DeleteMutator<
|
||||
super(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
entityConfiguration,
|
||||
idField,
|
||||
entityClass,
|
||||
privacyPolicy,
|
||||
entityLoaderFactory,
|
||||
@@ -274,10 +331,10 @@ export class DeleteMutator<
|
||||
|
||||
const id = this.entity.getID();
|
||||
|
||||
await this.databaseAdapter.deleteAsync(this.queryContext, this.entityConfiguration.idField, id);
|
||||
await this.databaseAdapter.deleteAsync(this.queryContext, this.idField, id);
|
||||
|
||||
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
|
||||
await entityLoader.invalidateFieldsAsync(this.entity.getAllFields());
|
||||
await entityLoader.invalidateFieldsAsync(this.entity.getAllDatabaseFields());
|
||||
|
||||
return result();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Entity, { IEntityClass } from './Entity';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
|
||||
import EntityLoaderFactory from './EntityLoaderFactory';
|
||||
import { CreateMutator, UpdateMutator, DeleteMutator } from './EntityMutator';
|
||||
@@ -15,17 +14,25 @@ export default class EntityMutatorFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends Entity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly idField: keyof TFields,
|
||||
private readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly privacyPolicy: TPrivacyPolicy,
|
||||
private readonly entityLoaderFactory: EntityLoaderFactory<
|
||||
@@ -33,7 +40,8 @@ export default class EntityMutatorFactory<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly databaseAdapter: EntityDatabaseAdapter<TFields>,
|
||||
private readonly metricsAdapter: IEntityMetricsAdapter
|
||||
@@ -48,11 +56,11 @@ export default class EntityMutatorFactory<
|
||||
forCreate(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext
|
||||
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return new CreateMutator(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
this.entityConfiguration,
|
||||
this.idField,
|
||||
this.entityClass,
|
||||
this.privacyPolicy,
|
||||
this.entityLoaderFactory,
|
||||
@@ -70,17 +78,17 @@ export default class EntityMutatorFactory<
|
||||
forUpdate(
|
||||
existingEntity: TEntity,
|
||||
queryContext: EntityQueryContext
|
||||
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return new UpdateMutator(
|
||||
existingEntity.getViewerContext(),
|
||||
queryContext,
|
||||
this.entityConfiguration,
|
||||
this.idField,
|
||||
this.entityClass,
|
||||
this.privacyPolicy,
|
||||
this.entityLoaderFactory,
|
||||
this.databaseAdapter,
|
||||
this.metricsAdapter,
|
||||
existingEntity.getAllFields()
|
||||
existingEntity.getAllDatabaseFields()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +100,11 @@ export default class EntityMutatorFactory<
|
||||
forDelete(
|
||||
existingEntity: TEntity,
|
||||
queryContext: EntityQueryContext
|
||||
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return new DeleteMutator(
|
||||
existingEntity.getViewerContext(),
|
||||
queryContext,
|
||||
this.entityConfiguration,
|
||||
this.idField,
|
||||
this.entityClass,
|
||||
this.privacyPolicy,
|
||||
this.entityLoaderFactory,
|
||||
|
||||
@@ -14,18 +14,23 @@ export type EntityPrivacyPolicyEvaluator<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> =
|
||||
| {
|
||||
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE;
|
||||
}
|
||||
| {
|
||||
mode: EntityPrivacyPolicyEvaluationMode.DRY_RUN;
|
||||
denyHandler: (error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity>) => void;
|
||||
denyHandler: (
|
||||
error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
||||
) => void;
|
||||
}
|
||||
| {
|
||||
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG;
|
||||
denyHandler: (error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity>) => void;
|
||||
denyHandler: (
|
||||
error: EntityNotAuthorizedError<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export enum EntityAuthorizationAction {
|
||||
@@ -50,40 +55,46 @@ export enum EntityAuthorizationAction {
|
||||
*
|
||||
* ```
|
||||
* foreach rule in rules:
|
||||
* authorized if rule allows
|
||||
* return authorized if rule allows
|
||||
* return not authorized if rule denies
|
||||
* continue to next rule if rule skips
|
||||
* not authorized if rule denies
|
||||
* return not authorized if all rules skip
|
||||
* ```
|
||||
*/
|
||||
export default abstract class EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
protected readonly createRules: readonly PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>[] = [];
|
||||
protected readonly readRules: readonly PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>[] = [];
|
||||
protected readonly updateRules: readonly PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>[] = [];
|
||||
protected readonly deleteRules: readonly PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>[] = [];
|
||||
|
||||
/**
|
||||
@@ -96,7 +107,7 @@ export default abstract class EntityPrivacyPolicy<
|
||||
*/
|
||||
protected getPrivacyPolicyEvaluator(
|
||||
_viewerContext: TViewerContext
|
||||
): EntityPrivacyPolicyEvaluator<TFields, TID, TViewerContext, TEntity> {
|
||||
): EntityPrivacyPolicyEvaluator<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
return {
|
||||
mode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
|
||||
};
|
||||
@@ -191,7 +202,7 @@ export default abstract class EntityPrivacyPolicy<
|
||||
}
|
||||
|
||||
private async authorizeForRulesetAsync(
|
||||
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>[],
|
||||
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
entity: TEntity,
|
||||
@@ -243,7 +254,7 @@ export default abstract class EntityPrivacyPolicy<
|
||||
}
|
||||
|
||||
private async authorizeForRulesetInnerAsync(
|
||||
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>[],
|
||||
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
entity: TEntity,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
|
||||
|
||||
/**
|
||||
* A database adapter provider vends database adapters for a particular database adapter type.
|
||||
* Allows for passing global configuration to databse adapters, making testing easier.
|
||||
*/
|
||||
export default interface IEntityDatabaseAdapterProvider {
|
||||
/**
|
||||
* Vend a database adapter.
|
||||
*/
|
||||
getDatabaseAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityDatabaseAdapter<TFields>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import invariant from 'invariant';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { IEntityClass } from './Entity';
|
||||
import EntityAssociationLoader from './EntityAssociationLoader';
|
||||
@@ -15,8 +16,14 @@ import ViewerContext from './ViewerContext';
|
||||
* - Entities representing SQL views.
|
||||
* - Entities representing immutable tables.
|
||||
*/
|
||||
export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extends ViewerContext> {
|
||||
export default abstract class ReadonlyEntity<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
private readonly id: TID;
|
||||
private readonly rawFields: Readonly<Pick<TFields, TSelectedFields>>;
|
||||
|
||||
/**
|
||||
* Constructs an instance of an Entity.
|
||||
@@ -27,13 +34,17 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
*/
|
||||
constructor(
|
||||
private readonly viewerContext: TViewerContext,
|
||||
private readonly rawFields: Readonly<TFields>
|
||||
private readonly databaseFields: Readonly<TFields>
|
||||
) {
|
||||
const idField = (this.constructor as any).getCompanionDefinition().entityConfiguration
|
||||
.idField as keyof TFields;
|
||||
const id = rawFields[idField];
|
||||
.idField as keyof Pick<TFields, TSelectedFields>;
|
||||
const id = databaseFields[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 {
|
||||
@@ -57,7 +68,13 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
/**
|
||||
* @returns {@link EntityAssociationLoader} for this entity
|
||||
*/
|
||||
associationLoader(): EntityAssociationLoader<TFields, TID, TViewerContext, this> {
|
||||
associationLoader(): EntityAssociationLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
this,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new EntityAssociationLoader(this);
|
||||
}
|
||||
|
||||
@@ -66,17 +83,26 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
* @param fieldName - the field to get
|
||||
* @returns the value of the field or undefined if not loaded with that field
|
||||
*/
|
||||
getField<K extends keyof TFields>(fieldName: K): TFields[K] {
|
||||
getField<K extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: K
|
||||
): Pick<TFields, TSelectedFields>[K] {
|
||||
return this.rawFields[fieldName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns all underlying fields from this entity's data
|
||||
*/
|
||||
getAllFields(): Readonly<TFields> {
|
||||
getAllFields(): Readonly<Pick<TFields, TSelectedFields>> {
|
||||
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
|
||||
@@ -86,10 +112,24 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2
|
||||
): EntityQueryContext {
|
||||
return viewerContext
|
||||
@@ -109,10 +149,24 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2,
|
||||
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<TResult>
|
||||
): Promise<TResult> {
|
||||
@@ -132,16 +186,30 @@ export default abstract class ReadonlyEntity<TFields, TID, TViewerContext extend
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMViewerContext2 extends TMViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields = keyof TMFields
|
||||
>(
|
||||
this: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>,
|
||||
this: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>,
|
||||
viewerContext: TMViewerContext2,
|
||||
queryContext: EntityQueryContext = viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getQueryContextProvider()
|
||||
.getRegularEntityQueryContext()
|
||||
): EntityLoader<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
|
||||
): EntityLoader<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
|
||||
return viewerContext
|
||||
.getViewerScopedEntityCompanionForClass(this)
|
||||
.getLoaderFactory()
|
||||
|
||||
@@ -29,11 +29,32 @@ export default class ViewerContext {
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext extends ViewerContext,
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<TMFields, TMID, TMViewerContext, TMEntity>
|
||||
TMEntity extends ReadonlyEntity<TMFields, TMID, TMViewerContext, TMSelectedFields>,
|
||||
TMPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMSelectedFields
|
||||
>,
|
||||
TMSelectedFields extends keyof TMFields
|
||||
>(
|
||||
entityClass: IEntityClass<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy>
|
||||
): ViewerScopedEntityCompanion<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy> {
|
||||
entityClass: IEntityClass<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
>
|
||||
): ViewerScopedEntityCompanion<
|
||||
TMFields,
|
||||
TMID,
|
||||
TMViewerContext,
|
||||
TMEntity,
|
||||
TMPrivacyPolicy,
|
||||
TMSelectedFields
|
||||
> {
|
||||
return this.viewerScopedEntityCompanionProvider.getViewerScopedCompanionForEntity(
|
||||
entityClass,
|
||||
entityClass.getCompanionDefinition()
|
||||
|
||||
@@ -14,8 +14,15 @@ export default class ViewerScopedEntityCompanion<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityCompanion: EntityCompanion<
|
||||
@@ -23,7 +30,8 @@ export default class ViewerScopedEntityCompanion<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly viewerContext: TViewerContext
|
||||
) {}
|
||||
@@ -36,7 +44,8 @@ export default class ViewerScopedEntityCompanion<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new ViewerScopedEntityLoaderFactory(
|
||||
this.entityCompanion.getLoaderFactory(),
|
||||
@@ -52,7 +61,8 @@ export default class ViewerScopedEntityCompanion<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new ViewerScopedEntityMutatorFactory(
|
||||
this.entityCompanion.getMutatorFactory(),
|
||||
|
||||
@@ -25,18 +25,40 @@ export default class ViewerScopedEntityCompanionProvider {
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
>(
|
||||
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
entityCompanionDefinition: EntityCompanionDefinition<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>
|
||||
): ViewerScopedEntityCompanion<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): ViewerScopedEntityCompanion<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new ViewerScopedEntityCompanion(
|
||||
this.entityCompanionProvider.getCompanionForEntity(entityClass, entityCompanionDefinition),
|
||||
this.viewerContext as TViewerContext
|
||||
|
||||
@@ -12,8 +12,15 @@ export default class ViewerScopedEntityLoaderFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityLoaderFactory: EntityLoaderFactory<
|
||||
@@ -21,14 +28,15 @@ export default class ViewerScopedEntityLoaderFactory<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly viewerContext: TViewerContext
|
||||
) {}
|
||||
|
||||
forLoad(
|
||||
queryContext: EntityQueryContext
|
||||
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,15 @@ export default class ViewerScopedEntityMutatorFactory<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<TFields, TID, TViewerContext, TEntity>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityMutatorFactory: EntityMutatorFactory<
|
||||
@@ -21,28 +28,29 @@ export default class ViewerScopedEntityMutatorFactory<
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly viewerContext: TViewerContext
|
||||
) {}
|
||||
|
||||
forCreate(
|
||||
queryContext: EntityQueryContext
|
||||
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): CreateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return this.entityMutatorFactory.forCreate(this.viewerContext, queryContext);
|
||||
}
|
||||
|
||||
forUpdate(
|
||||
existingEntity: TEntity,
|
||||
queryContext: EntityQueryContext
|
||||
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): UpdateMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return this.entityMutatorFactory.forUpdate(existingEntity, queryContext);
|
||||
}
|
||||
|
||||
forDelete(
|
||||
existingEntity: TEntity,
|
||||
queryContext: EntityQueryContext
|
||||
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy> {
|
||||
): DeleteMutator<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
|
||||
return this.entityMutatorFactory.forDelete(existingEntity, queryContext);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import EntityLoader from '../EntityLoader';
|
||||
describe(EnforcingEntityLoader, () => {
|
||||
describe('loadManyByFieldEqualingManyAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
|
||||
new Map(
|
||||
@@ -24,7 +24,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
|
||||
new Map(
|
||||
@@ -49,7 +49,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadManyByFieldEqualingAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
|
||||
result(rejection),
|
||||
@@ -62,7 +62,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
|
||||
result(resolved),
|
||||
@@ -77,7 +77,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadByFieldEqualingAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(rejection)
|
||||
@@ -90,7 +90,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(resolved)
|
||||
@@ -103,7 +103,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns null when result is successful and no entity is found', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = null;
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(resolved)
|
||||
@@ -116,7 +116,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('throws when multiple matching entities are found', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const multipleEntitiesError = new Error();
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenReject(
|
||||
multipleEntitiesError
|
||||
@@ -131,7 +131,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadByIDAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(rejection));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
@@ -140,7 +140,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
@@ -151,7 +151,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadByIDNullableAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(rejection));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
@@ -162,7 +162,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
@@ -173,7 +173,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns null when non-existent object', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = null;
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
@@ -186,7 +186,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadManyByIDsAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
@@ -201,7 +201,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
@@ -224,7 +224,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadManyByFieldEqualityConjunction', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(
|
||||
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
|
||||
@@ -237,7 +237,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(
|
||||
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
|
||||
@@ -252,7 +252,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
|
||||
describe('loadManyByRawWhereClause', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(
|
||||
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
|
||||
@@ -265,7 +265,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any>>(EntityLoader);
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(
|
||||
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
|
||||
|
||||
@@ -96,6 +96,8 @@ const testEntityConfiguration = new EntityConfiguration<TestEntityFields>({
|
||||
columnName: 'custom_id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
@@ -134,13 +136,11 @@ class SimpleTestDenyUpdateEntity extends Entity<TestEntityFields, string, Viewer
|
||||
}
|
||||
}
|
||||
|
||||
const simpleTestDenyUpdateEntityCompanion = {
|
||||
const simpleTestDenyUpdateEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: SimpleTestDenyUpdateEntity,
|
||||
entityConfiguration: testEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: SimpleTestDenyUpdateEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
|
||||
class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
@@ -154,10 +154,8 @@ class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, Viewer
|
||||
}
|
||||
}
|
||||
|
||||
const simpleTestDenyDeleteEntityCompanion = {
|
||||
const simpleTestDenyDeleteEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: SimpleTestDenyDeleteEntity,
|
||||
entityConfiguration: testEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: SimpleTestDenyDeleteEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -26,8 +27,28 @@ describe(EntityAssociationLoader, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadRelatedEntityByFieldEqualingAsync', () => {
|
||||
it('loads associated entities by field equaling', async () => {
|
||||
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 () => {
|
||||
const companionProvider = createUnitTestEntityCompanionProvider();
|
||||
const viewerContext = new TestViewerContext(companionProvider);
|
||||
const testOtherEntity = await enforceAsyncResult(
|
||||
@@ -57,6 +78,32 @@ 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 = {
|
||||
const blahCompanion = new EntityCompanionDefinition({
|
||||
entityClass: BlahEntity,
|
||||
entityConfiguration: new EntityConfiguration<BlahFields>({
|
||||
idField: 'id',
|
||||
@@ -100,11 +100,11 @@ const blahCompanion = {
|
||||
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,26 +1,24 @@
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import EntityCompanion from '../EntityCompanion';
|
||||
import EntityLoaderFactory from '../EntityLoaderFactory';
|
||||
import EntityMutatorFactory from '../EntityMutatorFactory';
|
||||
import IEntityQueryContextProvider from '../IEntityQueryContextProvider';
|
||||
import EntityTableDataCoordinator from '../internal/EntityTableDataCoordinator';
|
||||
import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
|
||||
import TestEntity, {
|
||||
TestEntityPrivacyPolicy,
|
||||
testEntityConfiguration,
|
||||
} from '../testfixtures/TestEntity';
|
||||
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
|
||||
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
|
||||
|
||||
describe(EntityCompanion, () => {
|
||||
it('correctly instantiates mutator and loader factories', () => {
|
||||
const tableDataCoordinatorMock = mock(EntityTableDataCoordinator);
|
||||
when(tableDataCoordinatorMock.entityConfiguration).thenReturn(testEntityConfiguration);
|
||||
|
||||
const companion = new EntityCompanion(
|
||||
TestEntity,
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter,
|
||||
new NoCacheStubCacheAdapterProvider(),
|
||||
instance(tableDataCoordinatorMock),
|
||||
TestEntityPrivacyPolicy,
|
||||
instance(mock<IEntityQueryContextProvider>()),
|
||||
instance(mock<IEntityMetricsAdapter>())
|
||||
);
|
||||
expect(companion.getLoaderFactory()).toBeInstanceOf(EntityLoaderFactory);
|
||||
|
||||
@@ -22,6 +22,8 @@ const blahConfiguration = new EntityConfiguration<BlahFields>({
|
||||
columnName: 'hello',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
class Blah1Entity extends Entity<BlahFields, string, ViewerContext> {
|
||||
@@ -61,24 +63,20 @@ class NoOpTest2PrivacyPolicy extends EntityPrivacyPolicy<
|
||||
Blah2Entity
|
||||
> {}
|
||||
|
||||
const blah1CompanionDefinition = {
|
||||
const blah1CompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: Blah1Entity,
|
||||
entityConfiguration: blahConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: NoOpTest1PrivacyPolicy,
|
||||
};
|
||||
});
|
||||
|
||||
const blah2CompanionDefinition = {
|
||||
const blah2CompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: Blah2Entity,
|
||||
entityConfiguration: blahConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: NoOpTest2PrivacyPolicy,
|
||||
};
|
||||
});
|
||||
|
||||
describe(EntityCompanionProvider, () => {
|
||||
it('returns different instances for different entity types', () => {
|
||||
it('returns different instances for different entity types, but share table data coordinators', () => {
|
||||
const entityCompanionProvider = createUnitTestEntityCompanionProvider();
|
||||
const companion1 = entityCompanionProvider.getCompanionForEntity(
|
||||
Blah1Entity,
|
||||
@@ -91,5 +89,6 @@ describe(EntityCompanionProvider, () => {
|
||||
);
|
||||
|
||||
expect(companion1).not.toEqual(companion2);
|
||||
expect(companion1['tableDataCoordinator']).toEqual(companion2['tableDataCoordinator']);
|
||||
});
|
||||
});
|
||||
+9
@@ -1,3 +1,4 @@
|
||||
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from '../EntityCompanionProvider';
|
||||
import EntityConfiguration from '../EntityConfiguration';
|
||||
import { UUIDField, StringField } from '../EntityFields';
|
||||
|
||||
@@ -27,11 +28,15 @@ 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', () => {
|
||||
@@ -48,6 +53,8 @@ describe(EntityConfiguration, () => {
|
||||
columnName: 'id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
expect(entityConfiguration.cacheKeyVersion).toEqual(0);
|
||||
});
|
||||
@@ -61,6 +68,8 @@ describe(EntityConfiguration, () => {
|
||||
columnName: 'id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
cacheKeyVersion: 100,
|
||||
});
|
||||
expect(entityConfiguration.cacheKeyVersion).toEqual(100);
|
||||
@@ -23,22 +23,33 @@ describe(EntityLoader, () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'huh',
|
||||
dateField: dateToInsert,
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
const privacyPolicy = new TestEntityPrivacyPolicy();
|
||||
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
@@ -52,7 +63,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManager
|
||||
@@ -87,29 +98,40 @@ describe(EntityLoader, () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
stringField: 'huh',
|
||||
numberField: 4,
|
||||
testIndexedField: '4',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
stringField: 'huh',
|
||||
numberField: 4,
|
||||
testIndexedField: '5',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'blah',
|
||||
stringField: 'huh2',
|
||||
numberField: 4,
|
||||
testIndexedField: '6',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
stringField: 'huh',
|
||||
numberField: 4,
|
||||
testIndexedField: '4',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
stringField: 'huh',
|
||||
numberField: 4,
|
||||
testIndexedField: '5',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'blah',
|
||||
stringField: 'huh2',
|
||||
numberField: 4,
|
||||
testIndexedField: '6',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -122,7 +144,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManager
|
||||
@@ -152,15 +174,26 @@ describe(EntityLoader, () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
stringField: 'huh',
|
||||
testIndexedField: '1',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
stringField: 'huh',
|
||||
testIndexedField: '1',
|
||||
numberField: 3,
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -173,7 +206,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManager
|
||||
@@ -192,7 +225,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManagerInstance
|
||||
@@ -214,7 +247,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManagerInstance
|
||||
@@ -247,7 +280,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManagerInstance
|
||||
@@ -276,7 +309,7 @@ describe(EntityLoader, () => {
|
||||
const entityLoader = new EntityLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManagerInstance
|
||||
|
||||
@@ -43,7 +43,8 @@ const createEntityMutatorFactory = (
|
||||
string,
|
||||
ViewerContext,
|
||||
TestEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
TestEntityPrivacyPolicy,
|
||||
keyof TestFields
|
||||
>;
|
||||
entityMutatorFactory: EntityMutatorFactory<
|
||||
TestFields,
|
||||
@@ -57,7 +58,10 @@ const createEntityMutatorFactory = (
|
||||
const privacyPolicy = new TestEntityPrivacyPolicy();
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
existingObjects
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([[testEntityConfiguration.tableName, existingObjects]])
|
||||
)
|
||||
);
|
||||
const metricsAdapter = new NoOpEntityMetricsAdapter();
|
||||
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
|
||||
@@ -70,13 +74,13 @@ const createEntityMutatorFactory = (
|
||||
metricsAdapter
|
||||
);
|
||||
const entityLoaderFactory = new EntityLoaderFactory(
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
dataManager
|
||||
);
|
||||
const entityMutatorFactory = new EntityMutatorFactory(
|
||||
testEntityConfiguration,
|
||||
testEntityConfiguration.idField,
|
||||
TestEntity,
|
||||
privacyPolicy,
|
||||
entityLoaderFactory,
|
||||
@@ -256,7 +260,8 @@ describe(EntityMutatorFactory, () => {
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestEntityPrivacyPolicy
|
||||
SimpleTestEntityPrivacyPolicy,
|
||||
keyof SimpleTestFields
|
||||
>
|
||||
>(EntityLoaderFactory)
|
||||
);
|
||||
@@ -286,7 +291,7 @@ describe(EntityMutatorFactory, () => {
|
||||
).thenReject(rejectionError);
|
||||
|
||||
const entityMutatorFactory = new EntityMutatorFactory(
|
||||
simpleTestEntityConfiguration,
|
||||
simpleTestEntityConfiguration.idField,
|
||||
SimpleTestEntity,
|
||||
instance(privacyPolicyMock),
|
||||
entityLoaderFactory,
|
||||
@@ -333,7 +338,8 @@ describe(EntityMutatorFactory, () => {
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestEntityPrivacyPolicy
|
||||
SimpleTestEntityPrivacyPolicy,
|
||||
keyof SimpleTestFields
|
||||
>
|
||||
>(EntityLoaderFactory)
|
||||
);
|
||||
@@ -349,7 +355,7 @@ describe(EntityMutatorFactory, () => {
|
||||
);
|
||||
|
||||
const entityMutatorFactory = new EntityMutatorFactory(
|
||||
simpleTestEntityConfiguration,
|
||||
simpleTestEntityConfiguration.idField,
|
||||
SimpleTestEntity,
|
||||
privacyPolicy,
|
||||
entityLoaderFactory,
|
||||
|
||||
@@ -224,7 +224,7 @@ class EmptyPolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContext,
|
||||
protected readonly deleteRules = [];
|
||||
}
|
||||
|
||||
const blahEntityCompanionDefinition = {
|
||||
const blahEntityCompanionDefinition = new EntityCompanionDefinition({
|
||||
entityClass: BlahEntity,
|
||||
entityConfiguration: new EntityConfiguration<BlahFields>({
|
||||
idField: 'id',
|
||||
@@ -234,11 +234,11 @@ const blahEntityCompanionDefinition = {
|
||||
columnName: 'id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
}),
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: AlwaysDenyPolicy,
|
||||
};
|
||||
});
|
||||
|
||||
describe(EntityPrivacyPolicy, () => {
|
||||
it('throws EntityNotAuthorizedError when deny', async () => {
|
||||
|
||||
@@ -11,7 +11,14 @@ describe(ViewerScopedEntityCompanion, () => {
|
||||
it('returns viewer scoped loader and mutator factory', () => {
|
||||
const vc = instance(mock(ViewerContext));
|
||||
const entityCompanion = mock<
|
||||
EntityCompanion<TestFields, string, ViewerContext, TestEntity, TestEntityPrivacyPolicy>
|
||||
EntityCompanion<
|
||||
TestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
TestEntity,
|
||||
TestEntityPrivacyPolicy,
|
||||
keyof TestFields
|
||||
>
|
||||
>();
|
||||
const viewerScopedEntityCompanion = new ViewerScopedEntityCompanion(entityCompanion, vc);
|
||||
expect(viewerScopedEntityCompanion.getLoaderFactory()).toBeInstanceOf(
|
||||
|
||||
@@ -9,13 +9,17 @@ describe(ViewerScopedEntityLoaderFactory, () => {
|
||||
it('correctly scopes viewer to entity loads', async () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityQueryContext));
|
||||
const baseLoader = mock<EntityLoaderFactory<any, any, any, any, any>>(EntityLoaderFactory);
|
||||
const baseLoader = mock<EntityLoaderFactory<any, any, any, any, any, any>>(EntityLoaderFactory);
|
||||
const baseLoaderInstance = instance(baseLoader);
|
||||
|
||||
const viewerScopedEntityLoader = new ViewerScopedEntityLoaderFactory<any, any, any, any, any>(
|
||||
baseLoaderInstance,
|
||||
viewerContext
|
||||
);
|
||||
const viewerScopedEntityLoader = new ViewerScopedEntityLoaderFactory<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>(baseLoaderInstance, viewerContext);
|
||||
|
||||
viewerScopedEntityLoader.forLoad(queryContext);
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ describe(ViewerScopedEntityMutatorFactory, () => {
|
||||
string,
|
||||
ViewerContext,
|
||||
TestEntity,
|
||||
TestEntityPrivacyPolicy
|
||||
TestEntityPrivacyPolicy,
|
||||
keyof TestFields
|
||||
>(baseMutatorFactoryInstance, viewerContext);
|
||||
|
||||
viewerScopedEntityLoader.forCreate(queryContext);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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'],
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
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'],
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export { default as EntityPrivacyPolicy } from './EntityPrivacyPolicy';
|
||||
export * from './EntityPrivacyPolicy';
|
||||
export * from './EntityQueryContext';
|
||||
export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterProvider';
|
||||
export { default as IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider';
|
||||
export { default as IEntityQueryContextProvider } from './IEntityQueryContextProvider';
|
||||
export { default as ReadonlyEntity } from './ReadonlyEntity';
|
||||
export { default as ViewerContext } from './ViewerContext';
|
||||
@@ -45,6 +46,7 @@ export * from './rules/PrivacyPolicyRule';
|
||||
export * from './utils/testing/PrivacyPolicyRuleTestUtils';
|
||||
export * from './utils/testing/StubCacheAdapter';
|
||||
export { default as StubDatabaseAdapter } from './utils/testing/StubDatabaseAdapter';
|
||||
export { default as StubDatabaseAdapterProvider } from './utils/testing/StubDatabaseAdapterProvider';
|
||||
export { default as StubQueryContextProvider } from './utils/testing/StubQueryContextProvider';
|
||||
export * from './utils/testing/createUnitTestEntityCompanionProvider';
|
||||
export * from './utils/collections/maps';
|
||||
@@ -0,0 +1,41 @@
|
||||
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,6 +115,7 @@ 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}`
|
||||
);
|
||||
|
||||
@@ -24,33 +24,44 @@ import StubQueryContextProvider from '../../utils/testing/StubQueryContextProvid
|
||||
import EntityDataManager from '../EntityDataManager';
|
||||
import ReadThroughEntityCache from '../ReadThroughEntityCache';
|
||||
|
||||
const objects: TestFields[] = [
|
||||
{
|
||||
customIdField: '1',
|
||||
testIndexedField: 'unique1',
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: '2',
|
||||
testIndexedField: 'unique2',
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: '3',
|
||||
testIndexedField: 'unique3',
|
||||
stringField: 'world',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
];
|
||||
const getObjects = (): Map<string, TestFields[]> =>
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: '1',
|
||||
testIndexedField: 'unique1',
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: '2',
|
||||
testIndexedField: 'unique2',
|
||||
stringField: 'hello',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: '3',
|
||||
testIndexedField: 'unique3',
|
||||
stringField: 'world',
|
||||
numberField: 1,
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
describe(EntityDataManager, () => {
|
||||
it('loads from db with a no-cache adapter', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
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);
|
||||
@@ -92,7 +103,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads from a caching adaptor', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -134,7 +150,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads from a caching adapter with a cache hit', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -171,7 +192,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads from data loader for same query', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -201,7 +227,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads and in-memory caches (dataloader) non-unique, non-cacheable loads', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -239,7 +270,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('invalidates objects', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -251,7 +287,7 @@ describe(EntityDataManager, () => {
|
||||
);
|
||||
const queryContext = StubQueryContextProvider.getRegularEntityQueryContext();
|
||||
|
||||
const objectInQuestion = objects[1];
|
||||
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1];
|
||||
|
||||
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
|
||||
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
|
||||
@@ -272,7 +308,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('invalidates all fields for an object', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -284,7 +325,7 @@ describe(EntityDataManager, () => {
|
||||
);
|
||||
const queryContext = StubQueryContextProvider.getRegularEntityQueryContext();
|
||||
|
||||
const objectInQuestion = objects[1];
|
||||
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1];
|
||||
|
||||
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
|
||||
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
|
||||
@@ -305,7 +346,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads only from DB when in transaction', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -337,7 +383,12 @@ describe(EntityDataManager, () => {
|
||||
});
|
||||
|
||||
it('loads by field equality conjunction and does not cache', async () => {
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
@@ -403,7 +454,12 @@ describe(EntityDataManager, () => {
|
||||
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
||||
const metricsAdapter = instance(metricsAdapterMock);
|
||||
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
|
||||
const objects = getObjects();
|
||||
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
objects
|
||||
);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
|
||||
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
|
||||
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
|
||||
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DatabaseAdapterFlavor, CacheAdapterFlavor } from '../../EntityCompanionProvider';
|
||||
import EntityConfiguration from '../../EntityConfiguration';
|
||||
import { UUIDField, StringField } from '../../EntityFields';
|
||||
import {
|
||||
@@ -37,6 +38,8 @@ const blahEntityConfiguration = new EntityConfiguration<BlahT>({
|
||||
columnName: 'transform_write',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
describe(getDatabaseFieldForEntityField, () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -16,6 +17,8 @@ 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,8 +10,9 @@ export default class AlwaysAllowPrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
|
||||
@@ -10,8 +10,9 @@ export default class AlwaysDenyPrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
|
||||
@@ -10,8 +10,9 @@ export default class AlwaysSkipPrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity> {
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
|
||||
@@ -39,7 +39,8 @@ export default abstract class PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
abstract async evaluateAsync(
|
||||
viewerContext: TViewerContext,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Entity from '../Entity';
|
||||
import {
|
||||
DatabaseAdapterFlavor,
|
||||
CacheAdapterFlavor,
|
||||
EntityCompanionDefinition,
|
||||
} from '../EntityCompanionProvider';
|
||||
import EntityConfiguration from '../EntityConfiguration';
|
||||
import { DateField } from '../EntityFields';
|
||||
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
||||
import ViewerContext from '../ViewerContext';
|
||||
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
||||
|
||||
export type DateIDTestFields = {
|
||||
id: Date;
|
||||
};
|
||||
|
||||
export const dateIDTestEntityConfiguration = new EntityConfiguration<DateIDTestFields>({
|
||||
idField: 'id',
|
||||
tableName: 'simple_test_entity_should_not_write_to_db',
|
||||
schema: {
|
||||
id: new DateField({
|
||||
columnName: 'custom_id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
export class DateIDTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
DateIDTestFields,
|
||||
Date,
|
||||
ViewerContext,
|
||||
DateIDTestEntity
|
||||
> {
|
||||
protected readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
|
||||
];
|
||||
protected readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
|
||||
];
|
||||
protected readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
|
||||
];
|
||||
protected readonly deleteRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
|
||||
];
|
||||
}
|
||||
|
||||
export default class DateIDTestEntity extends Entity<DateIDTestFields, Date, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
DateIDTestFields,
|
||||
Date,
|
||||
ViewerContext,
|
||||
DateIDTestEntity,
|
||||
DateIDTestEntityPrivacyPolicy
|
||||
> {
|
||||
return dateIDTestEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
export const dateIDTestEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: DateIDTestEntity,
|
||||
entityConfiguration: dateIDTestEntityConfiguration,
|
||||
privacyPolicyClass: DateIDTestEntityPrivacyPolicy,
|
||||
});
|
||||
@@ -14,6 +14,8 @@ 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',
|
||||
@@ -22,44 +24,75 @@ export const simpleTestEntityConfiguration = new EntityConfiguration<SimpleTestF
|
||||
columnName: 'custom_id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity
|
||||
SimpleTestEntity,
|
||||
SimpleTestFieldSelection
|
||||
> {
|
||||
protected readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestFieldSelection
|
||||
>(),
|
||||
];
|
||||
protected readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestFieldSelection
|
||||
>(),
|
||||
];
|
||||
protected readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestFieldSelection
|
||||
>(),
|
||||
];
|
||||
protected readonly deleteRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<SimpleTestFields, string, ViewerContext, SimpleTestEntity>(),
|
||||
new AlwaysAllowPrivacyPolicyRule<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestFieldSelection
|
||||
>(),
|
||||
];
|
||||
}
|
||||
|
||||
export default class SimpleTestEntity extends Entity<SimpleTestFields, string, ViewerContext> {
|
||||
export default class SimpleTestEntity extends Entity<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestFieldSelection
|
||||
> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
SimpleTestFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
SimpleTestEntity,
|
||||
SimpleTestEntityPrivacyPolicy
|
||||
SimpleTestEntityPrivacyPolicy,
|
||||
SimpleTestFieldSelection
|
||||
> {
|
||||
return testEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
export const testEntityCompanion = {
|
||||
export const testEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: SimpleTestEntity,
|
||||
entityConfiguration: simpleTestEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: SimpleTestEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
@@ -41,6 +41,8 @@ export const testEntityConfiguration = new EntityConfiguration<TestFields>({
|
||||
columnName: 'date_field',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
@@ -103,10 +105,8 @@ export default class TestEntity extends Entity<TestFields, string, ViewerContext
|
||||
}
|
||||
}
|
||||
|
||||
export const testEntityCompanion = {
|
||||
export const testEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: TestEntity,
|
||||
entityConfiguration: testEntityConfiguration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: TestEntityPrivacyPolicy,
|
||||
};
|
||||
});
|
||||
@@ -26,6 +26,8 @@ export const testEntity2Configuration = new EntityConfiguration<Test2Fields>({
|
||||
columnName: 'foreign_key',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
export class TestEntity2PrivacyPolicy extends EntityPrivacyPolicy<
|
||||
@@ -60,10 +62,8 @@ export default class TestEntity2 extends Entity<Test2Fields, string, ViewerConte
|
||||
}
|
||||
}
|
||||
|
||||
export const testEntity2Companion = {
|
||||
export const testEntity2Companion = new EntityCompanionDefinition({
|
||||
entityClass: TestEntity2,
|
||||
entityConfiguration: testEntity2Configuration,
|
||||
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
|
||||
privacyPolicyClass: TestEntity2PrivacyPolicy,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import Entity from '../Entity';
|
||||
import {
|
||||
DatabaseAdapterFlavor,
|
||||
CacheAdapterFlavor,
|
||||
EntityCompanionDefinition,
|
||||
} from '../EntityCompanionProvider';
|
||||
import EntityConfiguration from '../EntityConfiguration';
|
||||
import { NumberField } from '../EntityFields';
|
||||
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
||||
import ViewerContext from '../ViewerContext';
|
||||
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
||||
|
||||
export type NumberKeyFields = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export const numberKeyEntityConfiguration = new EntityConfiguration<NumberKeyFields>({
|
||||
idField: 'id',
|
||||
tableName: 'simple_test_entity_should_not_write_to_db',
|
||||
schema: {
|
||||
id: new NumberField({
|
||||
columnName: 'custom_id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
|
||||
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
|
||||
});
|
||||
|
||||
export class NumberKeyPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
NumberKeyFields,
|
||||
number,
|
||||
ViewerContext,
|
||||
NumberKeyEntity
|
||||
> {
|
||||
protected readonly readRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
|
||||
];
|
||||
protected readonly createRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
|
||||
];
|
||||
protected readonly updateRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
|
||||
];
|
||||
protected readonly deleteRules = [
|
||||
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
|
||||
];
|
||||
}
|
||||
|
||||
export default class NumberKeyEntity extends Entity<NumberKeyFields, number, ViewerContext> {
|
||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
||||
NumberKeyFields,
|
||||
number,
|
||||
ViewerContext,
|
||||
NumberKeyEntity,
|
||||
NumberKeyPrivacyPolicy
|
||||
> {
|
||||
return numberKeyEntityCompanion;
|
||||
}
|
||||
}
|
||||
|
||||
export const numberKeyEntityCompanion = new EntityCompanionDefinition({
|
||||
entityClass: NumberKeyEntity,
|
||||
entityConfiguration: numberKeyEntityConfiguration,
|
||||
privacyPolicyClass: NumberKeyPrivacyPolicy,
|
||||
});
|
||||
@@ -7,7 +7,8 @@ export interface Case<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
viewerContext: TViewerContext;
|
||||
queryContext: EntityQueryContext;
|
||||
@@ -18,8 +19,9 @@ type CaseMap<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
> = Map<string, () => Promise<Case<TFields, TID, TViewerContext, TEntity>>>;
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> = Map<string, () => Promise<Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>>>;
|
||||
|
||||
/**
|
||||
* Useful for defining test cases that have async preconditions.
|
||||
@@ -28,17 +30,18 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields
|
||||
>(
|
||||
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>,
|
||||
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
|
||||
{
|
||||
allowCases = new Map(),
|
||||
skipCases = new Map(),
|
||||
denyCases = new Map(),
|
||||
}: {
|
||||
allowCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
|
||||
skipCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
|
||||
denyCases?: CaseMap<TFields, TID, TViewerContext, TEntity>;
|
||||
allowCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
|
||||
skipCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
|
||||
denyCases?: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
|
||||
}
|
||||
): void => {
|
||||
describe(privacyPolicyRule.constructor.name, () => {
|
||||
@@ -84,26 +87,27 @@ export const describePrivacyPolicyRule = <
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext>
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields
|
||||
>(
|
||||
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity>,
|
||||
privacyPolicyRule: PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
|
||||
{
|
||||
allowCases = [],
|
||||
skipCases = [],
|
||||
denyCases = [],
|
||||
}: {
|
||||
allowCases?: Case<TFields, TID, TViewerContext, TEntity>[];
|
||||
skipCases?: Case<TFields, TID, TViewerContext, TEntity>[];
|
||||
denyCases?: Case<TFields, TID, TViewerContext, TEntity>[];
|
||||
allowCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
|
||||
skipCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
|
||||
denyCases?: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
|
||||
}
|
||||
): void => {
|
||||
const makeCasesMap = (
|
||||
cases: Case<TFields, TID, TViewerContext, TEntity>[]
|
||||
): CaseMap<TFields, TID, TViewerContext, TEntity> =>
|
||||
cases: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
|
||||
): CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields> =>
|
||||
cases.reduce(
|
||||
(
|
||||
acc: CaseMap<TFields, TID, TViewerContext, TEntity>,
|
||||
testCase: Case<TFields, TID, TViewerContext, TEntity>,
|
||||
acc: CaseMap<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
|
||||
testCase: Case<TFields, TID, TViewerContext, TEntity, TSelectedFields>,
|
||||
index
|
||||
) => {
|
||||
acc.set(`case ${index}`, async () => testCase);
|
||||
|
||||
@@ -14,7 +14,7 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
|
||||
}
|
||||
}
|
||||
|
||||
class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
public getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map();
|
||||
}
|
||||
@@ -48,15 +48,22 @@ 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);
|
||||
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration, this.cache);
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
cache: Map<string, Readonly<object>> = new Map();
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
readonly cache: Map<string, Readonly<object>>
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
}
|
||||
|
||||
public getFieldTransformerMap(): FieldTransformerMap {
|
||||
return new Map();
|
||||
@@ -102,7 +109,7 @@ class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFie
|
||||
|
||||
public async invalidateManyAsync<N extends keyof TFields>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly TFields[N][]
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<void> {
|
||||
fieldValues.forEach((fieldValue) => {
|
||||
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
||||
@@ -111,6 +118,11 @@ class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFie
|
||||
}
|
||||
|
||||
private createCacheKey<N extends keyof TFields>(fieldName: N, fieldValue: TFields[N]): string {
|
||||
return `${fieldName}:::${fieldValue}`;
|
||||
return [
|
||||
this.entityConfiguration.tableName,
|
||||
`v${this.entityConfiguration.cacheKeyVersion}`,
|
||||
fieldName as string,
|
||||
String(fieldValue),
|
||||
].join(':');
|
||||
}
|
||||
}
|
||||
@@ -8,27 +8,35 @@ import EntityDatabaseAdapter, {
|
||||
TableQuerySelectionModifiers,
|
||||
OrderByOrdering,
|
||||
} from '../../EntityDatabaseAdapter';
|
||||
import { StringField, NumberField } from '../../EntityFields';
|
||||
import {
|
||||
getDatabaseFieldForEntityField,
|
||||
FieldTransformerMap,
|
||||
transformFieldsToDatabaseObject,
|
||||
} from '../../internal/EntityFieldTransformationUtils';
|
||||
import { computeIfAbsent, mapMap } from '../collections/maps';
|
||||
|
||||
export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
private objects: Readonly<{ [key: string]: any }>[];
|
||||
|
||||
constructor(
|
||||
private readonly entityConfiguration2: EntityConfiguration<T>,
|
||||
prepopulatedObjects: Readonly<T>[] = []
|
||||
private readonly dataStore: Map<string, Readonly<{ [key: string]: any }>[]>
|
||||
) {
|
||||
super(entityConfiguration2);
|
||||
this.objects = prepopulatedObjects.map((o) =>
|
||||
transformFieldsToDatabaseObject(entityConfiguration2, this.getFieldTransformerMap(), o)
|
||||
}
|
||||
|
||||
public static convertFieldObjectsToDataStore<T>(
|
||||
entityConfiguration: EntityConfiguration<T>,
|
||||
dataStore: Map<string, Readonly<T>[]>
|
||||
): Map<string, Readonly<{ [key: string]: any }>[]> {
|
||||
return mapMap(dataStore, (objectsForTable) =>
|
||||
objectsForTable.map((objectForTable) =>
|
||||
transformFieldsToDatabaseObject(entityConfiguration, new Map(), objectForTable)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public getAllObjectsForTest(): Readonly<object>[] {
|
||||
return this.objects;
|
||||
public getObjectCollectionForTable(tableName: string): { [key: string]: any }[] {
|
||||
return computeIfAbsent(this.dataStore, tableName, () => []);
|
||||
}
|
||||
|
||||
protected getFieldTransformerMap(): FieldTransformerMap {
|
||||
@@ -37,13 +45,14 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
|
||||
protected async fetchManyWhereInternalAsync(
|
||||
_queryInterface: any,
|
||||
_tableName: string,
|
||||
tableName: string,
|
||||
tableField: string,
|
||||
tableValues: readonly any[]
|
||||
): Promise<object[]> {
|
||||
const objectCollection = this.getObjectCollectionForTable(tableName);
|
||||
return tableValues.reduce((acc, fieldValue) => {
|
||||
return acc.concat(
|
||||
this.objects.filter((obj) => {
|
||||
objectCollection.filter((obj) => {
|
||||
return obj[tableField] === fieldValue;
|
||||
})
|
||||
);
|
||||
@@ -83,12 +92,12 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
|
||||
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
||||
_queryInterface: any,
|
||||
_tableName: string,
|
||||
tableName: string,
|
||||
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
||||
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
||||
querySelectionModifiers: TableQuerySelectionModifiers
|
||||
): Promise<object[]> {
|
||||
let filteredObjects = this.objects;
|
||||
let filteredObjects = this.getObjectCollectionForTable(tableName);
|
||||
for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
|
||||
filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue);
|
||||
}
|
||||
@@ -127,26 +136,42 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter');
|
||||
}
|
||||
|
||||
private generateRandomID(): any {
|
||||
const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
|
||||
invariant(idSchemaField, `No schema field found for ${this.entityConfiguration2.idField}`);
|
||||
if (idSchemaField instanceof StringField) {
|
||||
return uuidv4();
|
||||
} else if (idSchemaField instanceof NumberField) {
|
||||
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported ID type for StubDatabaseAdapter: ${idSchemaField.constructor.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async insertInternalAsync(
|
||||
_queryInterface: any,
|
||||
_tableName: string,
|
||||
tableName: string,
|
||||
object: object
|
||||
): Promise<object[]> {
|
||||
const objectCollection = this.getObjectCollectionForTable(tableName);
|
||||
|
||||
const idField = getDatabaseFieldForEntityField(
|
||||
this.entityConfiguration2,
|
||||
this.entityConfiguration2.idField
|
||||
);
|
||||
const objectToInsert = {
|
||||
[idField]: uuidv4(),
|
||||
[idField]: this.generateRandomID(),
|
||||
...object,
|
||||
};
|
||||
this.objects.push(objectToInsert);
|
||||
objectCollection.push(objectToInsert);
|
||||
return [objectToInsert];
|
||||
}
|
||||
|
||||
protected async updateInternalAsync(
|
||||
_queryInterface: any,
|
||||
_tableName: string,
|
||||
tableName: string,
|
||||
tableIdField: string,
|
||||
id: any,
|
||||
object: object
|
||||
@@ -156,28 +181,32 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
|
||||
throw new Error(`Empty update (${tableIdField} = ${id})`);
|
||||
}
|
||||
|
||||
const objectIndex = this.objects.findIndex((obj) => {
|
||||
const objectCollection = this.getObjectCollectionForTable(tableName);
|
||||
|
||||
const objectIndex = objectCollection.findIndex((obj) => {
|
||||
return obj[tableIdField] === id;
|
||||
});
|
||||
invariant(objectIndex >= 0, 'should exist');
|
||||
this.objects[objectIndex] = {
|
||||
...this.objects[objectIndex],
|
||||
objectCollection[objectIndex] = {
|
||||
...objectCollection[objectIndex],
|
||||
...object,
|
||||
};
|
||||
return [this.objects[objectIndex]];
|
||||
return [objectCollection[objectIndex]];
|
||||
}
|
||||
|
||||
protected async deleteInternalAsync(
|
||||
_queryInterface: any,
|
||||
_tableName: string,
|
||||
tableName: string,
|
||||
tableIdField: string,
|
||||
id: any
|
||||
): Promise<number> {
|
||||
const objectIndex = this.objects.findIndex((obj) => {
|
||||
const objectCollection = this.getObjectCollectionForTable(tableName);
|
||||
|
||||
const objectIndex = objectCollection.findIndex((obj) => {
|
||||
return obj[tableIdField] === id;
|
||||
});
|
||||
invariant(objectIndex >= 0, 'should exist');
|
||||
this.objects.splice(objectIndex, 1);
|
||||
objectCollection.splice(objectIndex, 1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import EntityConfiguration from '../../EntityConfiguration';
|
||||
import EntityDatabaseAdapter from '../../EntityDatabaseAdapter';
|
||||
import IEntityDatabaseAdapterProvider from '../../IEntityDatabaseAdapterProvider';
|
||||
import StubDatabaseAdapter from './StubDatabaseAdapter';
|
||||
|
||||
export default class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
|
||||
private readonly objectCollection = new Map();
|
||||
|
||||
getDatabaseAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): EntityDatabaseAdapter<TFields> {
|
||||
return new StubDatabaseAdapter(entityConfiguration, this.objectCollection);
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,52 @@ import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { OrderByOrdering } from '../../../EntityDatabaseAdapter';
|
||||
import { EntityNonTransactionalQueryContext } from '../../../EntityQueryContext';
|
||||
import {
|
||||
DateIDTestFields,
|
||||
dateIDTestEntityConfiguration,
|
||||
} from '../../../testfixtures/DateIDTestEntity';
|
||||
import {
|
||||
SimpleTestFields,
|
||||
simpleTestEntityConfiguration,
|
||||
} from '../../../testfixtures/SimpleTestEntity';
|
||||
import { TestFields, testEntityConfiguration } from '../../../testfixtures/TestEntity';
|
||||
import {
|
||||
NumberKeyFields,
|
||||
numberKeyEntityConfiguration,
|
||||
} from '../../../testfixtures/TestEntityNumberKey';
|
||||
import StubDatabaseAdapter from '../StubDatabaseAdapter';
|
||||
|
||||
describe(StubDatabaseAdapter, () => {
|
||||
describe('fetchManyWhereAsync', () => {
|
||||
it('fetches many where', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'wat',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 5,
|
||||
stringField: 'huh',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'wat',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
const results = await databaseAdapter.fetchManyWhereAsync(queryContext, 'stringField', [
|
||||
'huh',
|
||||
@@ -36,29 +59,40 @@ describe(StubDatabaseAdapter, () => {
|
||||
describe('fetchManyByFieldEqualityConjunctionAsync', () => {
|
||||
it('supports conjuntions and query modifiers', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
const results = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
|
||||
queryContext,
|
||||
@@ -90,29 +124,40 @@ describe(StubDatabaseAdapter, () => {
|
||||
|
||||
it('supports multiple order bys', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'b',
|
||||
dateField: new Date(),
|
||||
},
|
||||
{
|
||||
customIdField: 'world',
|
||||
testIndexedField: 'h2',
|
||||
numberField: 3,
|
||||
stringField: 'c',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
const results = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
|
||||
queryContext,
|
||||
@@ -144,7 +189,10 @@ describe(StubDatabaseAdapter, () => {
|
||||
describe('fetchManyByRawWhereClauseAsync', () => {
|
||||
it('throws because it is unsupported', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, []);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
new Map()
|
||||
);
|
||||
await expect(
|
||||
databaseAdapter.fetchManyByRawWhereClauseAsync(queryContext, '', [], {})
|
||||
).rejects.toThrow();
|
||||
@@ -154,7 +202,10 @@ describe(StubDatabaseAdapter, () => {
|
||||
describe('insertAsync', () => {
|
||||
it('inserts a record', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, []);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
new Map()
|
||||
);
|
||||
const result = await databaseAdapter.insertAsync(queryContext, {
|
||||
stringField: 'hello',
|
||||
});
|
||||
@@ -162,22 +213,35 @@ describe(StubDatabaseAdapter, () => {
|
||||
stringField: 'hello',
|
||||
});
|
||||
|
||||
expect(databaseAdapter.getAllObjectsForTest()).toHaveLength(1);
|
||||
expect(
|
||||
databaseAdapter.getObjectCollectionForTable(testEntityConfiguration.tableName)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAsync', () => {
|
||||
it('updates a record', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
const result = await databaseAdapter.updateAsync(queryContext, 'customIdField', 'hello', {
|
||||
stringField: 'b',
|
||||
});
|
||||
@@ -191,19 +255,57 @@ describe(StubDatabaseAdapter, () => {
|
||||
describe('deleteAsync', () => {
|
||||
it('deletes an object', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
]);
|
||||
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
|
||||
testEntityConfiguration,
|
||||
StubDatabaseAdapter.convertFieldObjectsToDataStore(
|
||||
testEntityConfiguration,
|
||||
new Map([
|
||||
[
|
||||
testEntityConfiguration.tableName,
|
||||
[
|
||||
{
|
||||
customIdField: 'hello',
|
||||
testIndexedField: 'h1',
|
||||
numberField: 3,
|
||||
stringField: 'a',
|
||||
dateField: new Date(),
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
await databaseAdapter.deleteAsync(queryContext, 'customIdField', 'hello');
|
||||
|
||||
expect(databaseAdapter.getAllObjectsForTest()).toHaveLength(0);
|
||||
expect(
|
||||
databaseAdapter.getObjectCollectionForTable(testEntityConfiguration.tableName)
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports string and number IDs', async () => {
|
||||
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
|
||||
const databaseAdapter1 = new StubDatabaseAdapter<SimpleTestFields>(
|
||||
simpleTestEntityConfiguration,
|
||||
new Map()
|
||||
);
|
||||
const insertedObject1 = await databaseAdapter1.insertAsync(queryContext, {});
|
||||
expect(typeof insertedObject1.id).toBe('string');
|
||||
|
||||
const databaseAdapter2 = new StubDatabaseAdapter<NumberKeyFields>(
|
||||
numberKeyEntityConfiguration,
|
||||
new Map()
|
||||
);
|
||||
const insertedObject2 = await databaseAdapter2.insertAsync(queryContext, {});
|
||||
expect(typeof insertedObject2.id).toBe('number');
|
||||
|
||||
const databaseAdapter3 = new StubDatabaseAdapter<DateIDTestFields>(
|
||||
dateIDTestEntityConfiguration,
|
||||
new Map()
|
||||
);
|
||||
await expect(databaseAdapter3.insertAsync(queryContext, {})).rejects.toThrowError(
|
||||
'Unsupported ID type for StubDatabaseAdapter: DateField'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import EntityCompanionProvider, {
|
||||
import IEntityMetricsAdapter from '../../metrics/IEntityMetricsAdapter';
|
||||
import NoOpEntityMetricsAdapter from '../../metrics/NoOpEntityMetricsAdapter';
|
||||
import { InMemoryFullCacheStubCacheAdapterProvider } from './StubCacheAdapter';
|
||||
import StubDatabaseAdapter from './StubDatabaseAdapter';
|
||||
import StubDatabaseAdapterProvider from './StubDatabaseAdapterProvider';
|
||||
import StubQueryContextProvider from './StubQueryContextProvider';
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,7 @@ export const createUnitTestEntityCompanionProvider = (
|
||||
metricsAdapter,
|
||||
{
|
||||
[DatabaseAdapterFlavor.POSTGRES]: {
|
||||
adapter: StubDatabaseAdapter,
|
||||
adapterProvider: new StubDatabaseAdapterProvider(),
|
||||
queryContextProvider: StubQueryContextProvider,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -311,17 +311,17 @@
|
||||
which "^1.3.1"
|
||||
|
||||
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
|
||||
version "0.3.0"
|
||||
version "0.4.0"
|
||||
dependencies:
|
||||
ioredis "^4.16.1"
|
||||
|
||||
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
|
||||
version "0.3.0"
|
||||
version "0.4.0"
|
||||
dependencies:
|
||||
knex "^0.20.11"
|
||||
|
||||
"@expo/entity@file:packages/entity":
|
||||
version "0.3.0"
|
||||
version "0.4.0"
|
||||
dependencies:
|
||||
"@expo/results" "^0.3.0"
|
||||
dataloader "^2.0.0"
|
||||
@@ -1949,42 +1949,43 @@ anymatch@^2.0.0:
|
||||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
apollo-cache-control@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.10.0.tgz#ed056d1ce4fb520cab584890a2132a8429f346a1"
|
||||
integrity sha512-UIcPlrPdRTOKrF7kc5/WD5i6EVkGEEqgOK/fMj92fnnxR1KnQDiN82lqaxu02eZJvWjFJjik0JVJNXKOJXVrpQ==
|
||||
apollo-cache-control@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.11.0.tgz#7075492d04c5424e7c6769380b503e8f75b39d61"
|
||||
integrity sha512-dmRnQ9AXGw2SHahVGLzB/p4UW/taFBAJxifxubp8hqY5p9qdlSu4MPRq8zvV2ULMYf50rBtZyC4C+dZLqmHuHQ==
|
||||
dependencies:
|
||||
apollo-server-env "^2.4.3"
|
||||
graphql-extensions "^0.12.0"
|
||||
apollo-server-env "^2.4.4"
|
||||
apollo-server-plugin-base "^0.9.0"
|
||||
|
||||
apollo-datasource@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.0.tgz#2a6d82edb2eba21b4ddf21877009ba39ff821945"
|
||||
integrity sha512-Yja12BgNQhzuFGG/5Nw2MQe0hkuQy2+9er09HxeEyAf2rUDIPnhPrn1MDoZTB8MU7UGfjwITC+1ofzKkkrZobA==
|
||||
apollo-datasource@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.1.tgz#0b06da999ace50b7f5fe509f2a03f7de97974334"
|
||||
integrity sha512-h++/jQAY7GA+4TBM+7ezvctFmmGNLrAPf51KsagZj+NkT9qvxp585rdsuatynVbSl59toPK2EuVmc6ilmQHf+g==
|
||||
dependencies:
|
||||
apollo-server-caching "^0.5.1"
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-env "^2.4.4"
|
||||
|
||||
apollo-engine-reporting-protobuf@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.0.tgz#69fae95cdeacb39b9ff06851420cc2c2a7b9fc8b"
|
||||
integrity sha512-OgMwtLcuL+YAaO2xgkPbnRJnISLDSNE5F11p7oq+1ws+ws71CPfHAthDCxSObCPSALdhsLAGD0v3u3soBuNmMg==
|
||||
apollo-engine-reporting-protobuf@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.1.tgz#b6e66e6e382f9bcdc2ac8ed168b047eb1470c1a8"
|
||||
integrity sha512-TSfr9iAaInV8dhXkesdcmqsthRkVcJkzznmiM+1Ob/GScK7r6hBYCjVDt2613EHAg9SUzTOltIKlGD+N+GJRUw==
|
||||
dependencies:
|
||||
"@apollo/protobufjs" "^1.0.3"
|
||||
|
||||
apollo-engine-reporting@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.8.0.tgz#46a098fe4625b8c9f8866cc9601b3e2ab4ac4db3"
|
||||
integrity sha512-VPVpIGW6lbYXga6sqq/fG8ZaPR70bFuxvCov6X0npuVQPXwgZrzBp50cHx9uIaBVxDDxD3leeznsQbmF37RAww==
|
||||
apollo-engine-reporting@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-2.0.0.tgz#af007b4a8a481fa97baef0eac51a7824f1ec3310"
|
||||
integrity sha512-FvNwORsh3nxEfvQqd2xbd468a0q/R3kYar/Bk6YQdBX5qwqUhqmOcOSxLFk8Zb77HpwHij5CPpPWJb53TU1zcA==
|
||||
dependencies:
|
||||
apollo-engine-reporting-protobuf "^0.5.0"
|
||||
apollo-engine-reporting-protobuf "^0.5.1"
|
||||
apollo-graphql "^0.4.0"
|
||||
apollo-server-caching "^0.5.1"
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-env "^2.4.4"
|
||||
apollo-server-errors "^2.4.1"
|
||||
apollo-server-types "^0.4.0"
|
||||
apollo-server-plugin-base "^0.9.0"
|
||||
apollo-server-types "^0.5.0"
|
||||
async-retry "^1.2.1"
|
||||
graphql-extensions "^0.12.0"
|
||||
uuid "^8.0.0"
|
||||
|
||||
apollo-env@^0.6.4:
|
||||
version "0.6.4"
|
||||
@@ -2021,26 +2022,26 @@ apollo-server-caching@^0.5.1:
|
||||
dependencies:
|
||||
lru-cache "^5.0.0"
|
||||
|
||||
apollo-server-core@^2.13.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.13.0.tgz#d0273781d37d46dca0e96944dd25ac845094b1ec"
|
||||
integrity sha512-PqfsexbyObaQYb2jODs8v/XzrJcn+5mh0jA8ZfQCg5GENlua/CjeTZbRm2X0p3qpwc2E5jFAXSshrIWvhQAGZQ==
|
||||
apollo-server-core@^2.14.2:
|
||||
version "2.14.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.14.2.tgz#4ab055b96b8be7821a726c81e8aa412deb7f3644"
|
||||
integrity sha512-8G6Aoz+k+ecuQco1KNLFbMrxhe/8uR4AOaOYEvT/N5m/6lrkPYzvBAxbpRIub5AxEwpBPcIrI452rR3PD9DItA==
|
||||
dependencies:
|
||||
"@apollographql/apollo-tools" "^0.4.3"
|
||||
"@apollographql/graphql-playground-html" "1.6.24"
|
||||
"@types/graphql-upload" "^8.0.0"
|
||||
"@types/ws" "^7.0.0"
|
||||
apollo-cache-control "^0.10.0"
|
||||
apollo-datasource "^0.7.0"
|
||||
apollo-engine-reporting "^1.8.0"
|
||||
apollo-cache-control "^0.11.0"
|
||||
apollo-datasource "^0.7.1"
|
||||
apollo-engine-reporting "^2.0.0"
|
||||
apollo-server-caching "^0.5.1"
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-env "^2.4.4"
|
||||
apollo-server-errors "^2.4.1"
|
||||
apollo-server-plugin-base "^0.8.0"
|
||||
apollo-server-types "^0.4.0"
|
||||
apollo-tracing "^0.10.0"
|
||||
apollo-server-plugin-base "^0.9.0"
|
||||
apollo-server-types "^0.5.0"
|
||||
apollo-tracing "^0.11.0"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
graphql-extensions "^0.12.0"
|
||||
graphql-extensions "^0.12.2"
|
||||
graphql-tag "^2.9.2"
|
||||
graphql-tools "^4.0.0"
|
||||
graphql-upload "^8.0.2"
|
||||
@@ -2049,10 +2050,10 @@ apollo-server-core@^2.13.0:
|
||||
subscriptions-transport-ws "^0.9.11"
|
||||
ws "^6.0.0"
|
||||
|
||||
apollo-server-env@^2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
|
||||
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
|
||||
apollo-server-env@^2.4.4:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.4.tgz#12d2d0896dcb184478cba066c7a683ab18689ca1"
|
||||
integrity sha512-c2oddDS3lwAl6QNCIKCLEzt/dF9M3/tjjYRVdxOVN20TidybI7rAbnT4QOzf4tORnGXtiznEAvr/Kc9ahhKADg==
|
||||
dependencies:
|
||||
node-fetch "^2.1.2"
|
||||
util.promisify "^1.0.0"
|
||||
@@ -2062,10 +2063,10 @@ apollo-server-errors@^2.4.1:
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.4.1.tgz#16ad49de6c9134bfb2b7dede9842e73bb239dbe2"
|
||||
integrity sha512-7oEd6pUxqyWYUbQ9TA8tM0NU/3aGtXSEibo6+txUkuHe7QaxfZ2wHRp+pfT1LC1K3RXYjKj61/C2xEO19s3Kdg==
|
||||
|
||||
apollo-server-koa@^2.13.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-koa/-/apollo-server-koa-2.13.0.tgz#c0bb58881c36ac1925e150180b522c5ea3c31f24"
|
||||
integrity sha512-tNtDWvJWmOMJRX8Szo7lzWw7K+1l4j4Sc+Y2ZGfr83qSGi0CJM87DoRIPsCNXD2AQwiTJC2f8zKByVpS6NsoFQ==
|
||||
apollo-server-koa@^2.14.2:
|
||||
version "2.14.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-koa/-/apollo-server-koa-2.14.2.tgz#95d098dd6cf831e2e466908956c2dee8b8b6d59b"
|
||||
integrity sha512-pQuQPQeIkUdsjn3anRN5y1nZSM+tu/H2rCLae3l2gBZknj8ABJ20EeBKFjnB2/6OuHo3FQJSZVt9zeRbMfWKSw==
|
||||
dependencies:
|
||||
"@apollographql/graphql-playground-html" "1.6.24"
|
||||
"@koa/cors" "^2.2.1"
|
||||
@@ -2076,39 +2077,39 @@ apollo-server-koa@^2.13.0:
|
||||
"@types/koa-compose" "^3.2.2"
|
||||
"@types/koa__cors" "^2.2.1"
|
||||
accepts "^1.3.5"
|
||||
apollo-server-core "^2.13.0"
|
||||
apollo-server-types "^0.4.0"
|
||||
apollo-server-core "^2.14.2"
|
||||
apollo-server-types "^0.5.0"
|
||||
graphql-subscriptions "^1.0.0"
|
||||
graphql-tools "^4.0.0"
|
||||
koa "2.11.0"
|
||||
koa "2.12.0"
|
||||
koa-bodyparser "^4.2.1"
|
||||
koa-compose "^4.1.0"
|
||||
koa-router "^7.4.0"
|
||||
type-is "^1.6.16"
|
||||
|
||||
apollo-server-plugin-base@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.8.0.tgz#6ab055f5fd18b0c225643aafc46787d622e0a47c"
|
||||
integrity sha512-H8sJlOVJrF0IhYIFMv7NOgB6BFgqobXSZrj1y9ju6dq13OotsqcZC4fJOYc9oWzb/+/mqg/odtVioE71mj68yg==
|
||||
apollo-server-plugin-base@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.9.0.tgz#777f720a1ee827a66b8c159073ca30645f8bc625"
|
||||
integrity sha512-LWcPrsy2+xqwlNseh/QaGa/MPNopS8c4qGgh0g0cAn0lZBRrJ9Yab7dq+iQ6vdUBwIhUWYN6s9dwUWCZw2SL8g==
|
||||
dependencies:
|
||||
apollo-server-types "^0.4.0"
|
||||
apollo-server-types "^0.5.0"
|
||||
|
||||
apollo-server-types@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.4.0.tgz#74c2ed18ef46021f83a5a2dc0b844518b13044bf"
|
||||
integrity sha512-U+6qKCdrucVSMEVvLSqSwxIGr3VI6vcfbhpD86sdb8MgHHGH6egjNAcLrPVRk1AyXs8RV0Ysus+vlj8rpouBzA==
|
||||
apollo-server-types@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.5.0.tgz#51f39c5fa610ece8b07f1fbcf63c47d4ac150340"
|
||||
integrity sha512-zhtsqqqfdeoJQAfc41Sy6WnnBVxKNgZ34BKXf/Q+kXmw7rbZ/B5SG3SJMvj1iFsbzZxILmWdUsE9aD20lEr0bg==
|
||||
dependencies:
|
||||
apollo-engine-reporting-protobuf "^0.5.0"
|
||||
apollo-engine-reporting-protobuf "^0.5.1"
|
||||
apollo-server-caching "^0.5.1"
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-env "^2.4.4"
|
||||
|
||||
apollo-tracing@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.10.0.tgz#24fc911c0d5145e3ef20a6cdc8d0b807b8cc7eee"
|
||||
integrity sha512-yuqA1KT0FQUfzVK3ZIk0hRIE8eUKx9Oklq83AGQxLtS/oafBj/VOCZAtJNJkyEqMJxXQT9uIBtbfO1789Gczkw==
|
||||
apollo-tracing@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.11.0.tgz#8821eb60692f77c06660fb6bc147446f600aecfe"
|
||||
integrity sha512-I9IFb/8lkBW8ZwOAi4LEojfT7dMfUSkpnV8LHQI8Rcj0HtzL9HObQ3woBmzyGHdGHLFuD/6/VHyFD67SesSrJg==
|
||||
dependencies:
|
||||
apollo-server-env "^2.4.3"
|
||||
graphql-extensions "^0.12.0"
|
||||
apollo-server-env "^2.4.4"
|
||||
apollo-server-plugin-base "^0.9.0"
|
||||
|
||||
apollo-utilities@^1.0.1, apollo-utilities@^1.3.0:
|
||||
version "1.3.3"
|
||||
@@ -4312,14 +4313,14 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||
|
||||
graphql-extensions@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.0.tgz#c0102d7bbe94ad6fc39806fd09bd37868ac928a2"
|
||||
integrity sha512-kBRLtNeknrFl0W/UQQYebj6qnvb1E1RpQ2+C7Y8pwMc6yV8+9pWFx5RP0HzfeEuScCmK93i3H5sdPedoQWwENw==
|
||||
graphql-extensions@^0.12.2:
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.2.tgz#f22210e812939b7caa2127589f30e6a1c671540f"
|
||||
integrity sha512-vFaZua5aLiCOOzxfY5qzHZ6S52BCqW7VVOwzvV52Wb5edRm3dn6u+1MR9yYyEqUHSf8LvdhEojYlOkKiaQ4ghA==
|
||||
dependencies:
|
||||
"@apollographql/apollo-tools" "^0.4.3"
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-types "^0.4.0"
|
||||
apollo-server-env "^2.4.4"
|
||||
apollo-server-types "^0.5.0"
|
||||
|
||||
graphql-subscriptions@^1.0.0:
|
||||
version "1.1.0"
|
||||
@@ -5678,7 +5679,36 @@ koa-router@^8.0.8:
|
||||
path-to-regexp "1.x"
|
||||
urijs "^1.19.2"
|
||||
|
||||
[email protected]1.0, koa@^2.11.0:
|
||||
[email protected]2.0:
|
||||
version "2.12.0"
|
||||
resolved "https://registry.yarnpkg.com/koa/-/koa-2.12.0.tgz#c92bfb42defd86f365c31bf63fe918db11fc5c74"
|
||||
integrity sha512-WlUBj6PXoVhjI5ljMmlyK+eqkbVFW5XQu8twz6bd4WM2E67IwKgPMu5wIFXGxAsZT7sW5xAB54KhY8WAEkLPug==
|
||||
dependencies:
|
||||
accepts "^1.3.5"
|
||||
cache-content-type "^1.0.0"
|
||||
content-disposition "~0.5.2"
|
||||
content-type "^1.0.4"
|
||||
cookies "~0.8.0"
|
||||
debug "~3.1.0"
|
||||
delegates "^1.0.0"
|
||||
depd "^1.1.2"
|
||||
destroy "^1.0.4"
|
||||
encodeurl "^1.0.2"
|
||||
escape-html "^1.0.3"
|
||||
fresh "~0.5.2"
|
||||
http-assert "^1.3.0"
|
||||
http-errors "^1.6.3"
|
||||
is-generator-function "^1.0.7"
|
||||
koa-compose "^4.1.0"
|
||||
koa-convert "^1.2.0"
|
||||
on-finished "^2.3.0"
|
||||
only "~0.0.2"
|
||||
parseurl "^1.3.2"
|
||||
statuses "^1.5.0"
|
||||
type-is "^1.6.16"
|
||||
vary "^1.1.2"
|
||||
|
||||
koa@^2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4"
|
||||
integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==
|
||||
@@ -8721,6 +8751,11 @@ uuid@^7.0.1, uuid@^7.0.3:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||
|
||||
uuid@^8.0.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
||||
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
|
||||
|
||||
Reference in new issue
Block a user