mirror of
https://github.com/expo/entity.git
synced 2026-11-15 03:48:08 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b749a0633 | ||
|
|
d731b0ddb7 | ||
|
|
293868b1a7 |
No files matched your search
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* lock lru-cache to v6.0.0 ([#170](https://github.com/expo/entity/issues/170)) ([293868b](https://github.com/expo/entity/commit/293868b1a73df9f749216af68b1ae94ac22ac2b9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.25.2"
|
||||
"version": "0.25.3"
|
||||
}
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
"@types/ioredis": "^4.26.4",
|
||||
"@types/jest": "^26.0.10",
|
||||
"@types/jsbn": "^1.2.29",
|
||||
"@types/lru-cache": "^7.4.0",
|
||||
"@types/lru-cache": "^5.1.1",
|
||||
"@types/node": "^14.6.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"eslint-plugin-tsdoc": "^0.2.11",
|
||||
"jest": "^26.6.3",
|
||||
"lerna": "^4.0.0",
|
||||
"lru-cache": "^7.3.1",
|
||||
"lru-cache": "^6.0.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pg": "^8.6.0",
|
||||
"prettier": "^2.3.2",
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* lock lru-cache to v6.0.0 ([#170](https://github.com/expo/entity/issues/170)) ([293868b](https://github.com/expo/entity/commit/293868b1a73df9f749216af68b1ae94ac22ac2b9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
|
||||
|
||||
@@ -3,23 +3,26 @@
|
||||
Cross-request [LRU](https://github.com/isaacs/node-lru-cache) cache adapter for `@expo/entity`. Use
|
||||
this cache with caution - it is nonstandard. The cache is shared between requests in the node process.
|
||||
|
||||
Note: This uses version 6.0.0 of `node-lru-cache` since it the most tuned version for our use case (low TTL + LRU). Upgrading
|
||||
to 7.x will cause high memory usage for the entity cache adapter use case since it allocates fixed-size data structures up front to tune for the non-TTL use case: https://github.com/isaacs/node-lru-cache/issues/208.
|
||||
|
||||
[Documentation](https://expo.github.io/entity/modules/_expo_cache_adapter_local_memory.html)
|
||||
|
||||
## Why NOT use this cache
|
||||
|
||||
Because this is an in-memory cache, cross-box invalidation is not possible. Do not use this cache
|
||||
Because this is an in-memory cache, cross-machine invalidation is not possible. Do not use this cache
|
||||
if you have the following use cases:
|
||||
|
||||
- The objects stored have high mutability
|
||||
- The objects stored are mutable
|
||||
- Reading a stale object from the cache is not acceptable in your application
|
||||
- Cross-box invalidation is not possible
|
||||
- Cross-machine invalidation is not possible
|
||||
|
||||
## Typical use cases
|
||||
|
||||
If your application sees many requests fetching the same objects, you can save a trip to your cache
|
||||
cluster and backing datastore by using this in-memory cache. Here are some good use cases:
|
||||
|
||||
- The objects stored are mostly immutable
|
||||
- The objects stored are mostly immutable, and reading a stale object for a short TTL is acceptable
|
||||
- You have a low TTL setting in your cache
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-local-memory",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Cross-request local memory cache adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -30,9 +30,9 @@
|
||||
"@expo/entity": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": "^7.3.1"
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2"
|
||||
"@expo/entity": "^0.25.3"
|
||||
}
|
||||
}
|
||||
@@ -7,76 +7,6 @@ export const DOES_NOT_EXIST_LOCAL_MEMORY_CACHE = Symbol('doesNotExist');
|
||||
type LocalMemoryCacheValue<TFields> = Readonly<TFields> | typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
|
||||
export type LocalMemoryCache<TFields> = LRUCache<string, LocalMemoryCacheValue<TFields>>;
|
||||
|
||||
type LRUCacheOptionsV7<K, V> = {
|
||||
/**
|
||||
* the number of most recently used items to keep.
|
||||
* note that we may store fewer items than this if maxSize is hit.
|
||||
*/
|
||||
max: number;
|
||||
|
||||
/**
|
||||
* if you wish to track item size, you must provide a maxSize
|
||||
* note that we still will only keep up to max *actual items*,
|
||||
* so size tracking may cause fewer than max items to be stored.
|
||||
* At the extreme, a single item of maxSize size will cause everything
|
||||
* else in the cache to be dropped when it is added. Use with caution!
|
||||
* Note also that size tracking can negatively impact performance,
|
||||
* though for most cases, only minimally.
|
||||
*/
|
||||
|
||||
maxSize?: number;
|
||||
|
||||
/**
|
||||
* function to calculate size of items. useful if storing strings or
|
||||
* buffers or other items where memory size depends on the object itself.
|
||||
* also note that oversized items do NOT immediately get dropped from
|
||||
* the cache, though they will cause faster turnover in the storage.
|
||||
*/
|
||||
sizeCalculation?: (value: V, key: K) => number;
|
||||
|
||||
/**
|
||||
* function to call when the item is removed from the cache
|
||||
* Note that using this can negatively impact performance.
|
||||
*/
|
||||
dispose?: (value: V, key: K) => void;
|
||||
|
||||
/**
|
||||
* max time to live for items before they are considered stale
|
||||
* note that stale items are NOT preemptively removed by default,
|
||||
* and MAY live in the cache, contributing to its LRU max, long after
|
||||
* they have expired.
|
||||
* Also, as this cache is optimized for LRU/MRU operations, some of
|
||||
* the staleness/TTL checks will reduce performance, as they will incur
|
||||
* overhead by deleting items.
|
||||
* Must be a positive integer in ms, defaults to 0, which means "no TTL"
|
||||
*/
|
||||
ttl?: number;
|
||||
|
||||
/**
|
||||
* return stale items from cache.get() before disposing of them
|
||||
* boolean, default false
|
||||
*/
|
||||
allowStale?: boolean;
|
||||
|
||||
/**
|
||||
* update the age of items on cache.get(), renewing their TTL
|
||||
* boolean, default false
|
||||
*/
|
||||
updateAgeOnGet?: boolean;
|
||||
|
||||
/**
|
||||
* update the age of items on cache.has(), renewing their TTL
|
||||
* boolean, default false
|
||||
*/
|
||||
updateAgeOnHas?: boolean;
|
||||
|
||||
/**
|
||||
* update the "recently-used"-ness of items on cache.has()
|
||||
* boolean, default false
|
||||
*/
|
||||
updateRecencyOnHas?: boolean;
|
||||
};
|
||||
|
||||
export default class GenericLocalMemoryCacher<TFields> implements IEntityGenericCacher<TFields> {
|
||||
constructor(private readonly localMemoryCache: LocalMemoryCache<TFields>) {}
|
||||
|
||||
@@ -86,21 +16,17 @@ export default class GenericLocalMemoryCacher<TFields> implements IEntityGeneric
|
||||
const DEFAULT_LRU_CACHE_MAX_AGE_SECONDS = 10;
|
||||
const DEFAULT_LRU_CACHE_SIZE = 10000;
|
||||
const maxAgeSeconds = options.ttlSeconds ?? DEFAULT_LRU_CACHE_MAX_AGE_SECONDS;
|
||||
const lruCacheOptions: LRUCacheOptionsV7<string, TFields> = {
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
||||
max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
|
||||
maxSize: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
|
||||
sizeCalculation: (value: LocalMemoryCacheValue<TFields>) =>
|
||||
value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1,
|
||||
ttl: maxAgeSeconds * 1000, // convert to ms
|
||||
};
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>(lruCacheOptions as any);
|
||||
length: (value) => (value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1),
|
||||
maxAge: maxAgeSeconds * 1000, // convert to ms
|
||||
});
|
||||
}
|
||||
|
||||
static createNoOpCache<TFields>(): LocalMemoryCache<TFields> {
|
||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
||||
max: 1,
|
||||
maxSize: 1,
|
||||
sizeCalculation: () => 10, // make all things larger than max size
|
||||
max: 0,
|
||||
maxAge: -1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +68,7 @@ export default class GenericLocalMemoryCacher<TFields> implements IEntityGeneric
|
||||
|
||||
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
this.localMemoryCache.delete(key);
|
||||
this.localMemoryCache.del(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -5,8 +5,7 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
it('creates a cache with default options', () => {
|
||||
const cache = GenericLocalMemoryCacher.createLRUCache();
|
||||
expect(cache.max).toBe(10000);
|
||||
expect(cache.maxSize).toBe(10000);
|
||||
expect(cache.ttl).toBe(10000);
|
||||
expect(cache.maxAge).toBe(10000);
|
||||
});
|
||||
|
||||
it('respects specified options', () => {
|
||||
@@ -15,8 +14,7 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
maxSize: 10,
|
||||
});
|
||||
expect(cache.max).toBe(10);
|
||||
expect(cache.maxSize).toBe(10);
|
||||
expect(cache.ttl).toBe(3000);
|
||||
expect(cache.maxAge).toBe(3000);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+10
-12
@@ -32,9 +32,9 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
|
||||
entity1Created.getID()
|
||||
);
|
||||
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
@@ -97,15 +97,13 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(entity1Created.getID());
|
||||
|
||||
// load entity with a different request
|
||||
const viewerContext2 = new TestViewerContext(createLocalMemoryTestEntityCompanionProvider());
|
||||
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2).loadByIDAsync(
|
||||
entity1Created.getID()
|
||||
);
|
||||
|
||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
||||
LocalMemoryTestEntity,
|
||||
@@ -140,9 +138,9 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
|
||||
entity1Created.getID()
|
||||
);
|
||||
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-redis",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Redis cache adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -33,6 +33,6 @@
|
||||
"ioredis": "^4.27.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2"
|
||||
"@expo/entity": "^0.25.3"
|
||||
}
|
||||
}
|
||||
+10
-13
@@ -52,9 +52,9 @@ describe(RedisCacheAdapter, () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
const entity1 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
const entity1 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
|
||||
entity1Created.getID()
|
||||
);
|
||||
|
||||
const cachedJSON = await redisCacheAdapterContext.redisClient.get(
|
||||
cacheKeyMaker('id', entity1.getID())
|
||||
@@ -102,16 +102,14 @@ describe(RedisCacheAdapter, () => {
|
||||
);
|
||||
expect(entity1.getField('dateField')).toEqual(date);
|
||||
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1.getID());
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
|
||||
expect(entity2.getField('dateField')).toEqual(date);
|
||||
|
||||
// simulate new request
|
||||
const vc2 = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const entity3 = await RedisTestEntity.loader(vc2).enforcing().loadByIDAsync(entity1.getID());
|
||||
const entity3 = await RedisTestEntity.loader(vc2).loadByIDAsync(entity1.getID());
|
||||
expect(entity3.getField('dateField')).toEqual(date);
|
||||
});
|
||||
|
||||
@@ -122,18 +120,17 @@ describe(RedisCacheAdapter, () => {
|
||||
const entity1 = await enforceAsyncResult(
|
||||
RedisTestEntity.creator(viewerContext).setField('name', '').createAsync()
|
||||
);
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', '');
|
||||
const entity2 = await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
''
|
||||
);
|
||||
expect(entity2?.getID()).toEqual(entity1.getID());
|
||||
|
||||
// simulate new request
|
||||
const vc2 = new TestViewerContext(
|
||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
||||
);
|
||||
const entity3 = await RedisTestEntity.loader(vc2)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', '');
|
||||
const entity3 = await RedisTestEntity.loader(vc2).loadByFieldEqualingAsync('name', '');
|
||||
expect(entity3?.getID()).toEqual(entity1.getID());
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Knex database adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -33,7 +33,7 @@
|
||||
"knex": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity": "^0.25.3",
|
||||
"pg": "^8.7.1"
|
||||
}
|
||||
}
|
||||
+86
-111
@@ -50,9 +50,7 @@ describe('postgres entity integration', () => {
|
||||
PostgresTestEntity.updater(entity).setField('hasADog', false).updateAsync(),
|
||||
]);
|
||||
|
||||
const loadedEntity = await PostgresTestEntity.loader(vc)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity.getID());
|
||||
const loadedEntity = await PostgresTestEntity.loader(vc).loadByIDAsync(entity.getID());
|
||||
|
||||
expect(loadedEntity.getField('hasACat')).toBe(true);
|
||||
expect(loadedEntity.getField('hasADog')).toBe(false);
|
||||
@@ -185,26 +183,22 @@ describe('postgres entity integration', () => {
|
||||
.createAsync()
|
||||
);
|
||||
|
||||
const results = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([
|
||||
{
|
||||
fieldName: 'hasACat',
|
||||
fieldValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'hasADog',
|
||||
fieldValue: true,
|
||||
},
|
||||
]);
|
||||
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync([
|
||||
{
|
||||
fieldName: 'hasACat',
|
||||
fieldValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'hasADog',
|
||||
fieldValue: true,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
|
||||
const results2 = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'hasADog', fieldValues: [true, false] },
|
||||
]);
|
||||
const results2 = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
|
||||
[{ fieldName: 'hasADog', fieldValues: [true, false] }]
|
||||
);
|
||||
expect(results2).toHaveLength(3);
|
||||
});
|
||||
|
||||
@@ -217,9 +211,9 @@ describe('postgres entity integration', () => {
|
||||
|
||||
await enforceAsyncResult(PostgresTestEntity.creator(vc1).setField('name', 'c').createAsync());
|
||||
|
||||
const results = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([], {
|
||||
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
|
||||
[],
|
||||
{
|
||||
limit: 2,
|
||||
offset: 1,
|
||||
orderBy: [
|
||||
@@ -228,7 +222,8 @@ describe('postgres entity integration', () => {
|
||||
order: OrderByOrdering.DESCENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'a']);
|
||||
});
|
||||
@@ -260,28 +255,26 @@ describe('postgres entity integration', () => {
|
||||
.createAsync()
|
||||
);
|
||||
|
||||
const results = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]);
|
||||
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'name', fieldValue: null },
|
||||
]);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]!.getField('name')).toBeNull();
|
||||
|
||||
const results2 = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync(
|
||||
[
|
||||
{ fieldName: 'name', fieldValues: ['a', null] },
|
||||
{ fieldName: 'hasADog', fieldValue: true },
|
||||
const results2 = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
|
||||
[
|
||||
{ fieldName: 'name', fieldValues: ['a', null] },
|
||||
{ fieldName: 'hasADog', fieldValue: true },
|
||||
],
|
||||
{
|
||||
orderBy: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
order: OrderByOrdering.DESCENDING,
|
||||
},
|
||||
],
|
||||
{
|
||||
orderBy: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
order: OrderByOrdering.DESCENDING,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
expect(results2).toHaveLength(2);
|
||||
expect(results2.map((e) => e.getField('name'))).toEqual([null, 'a']);
|
||||
});
|
||||
@@ -298,9 +291,10 @@ describe('postgres entity integration', () => {
|
||||
.createAsync()
|
||||
);
|
||||
|
||||
const results = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByRawWhereClauseAsync('name = ?', ['hello']);
|
||||
const results = await PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync(
|
||||
'name = ?',
|
||||
['hello']
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
});
|
||||
@@ -316,9 +310,9 @@ describe('postgres entity integration', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByRawWhereClauseAsync('invalid_column = ?', ['hello'])
|
||||
PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [
|
||||
'hello',
|
||||
])
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -346,9 +340,10 @@ describe('postgres entity integration', () => {
|
||||
.createAsync()
|
||||
);
|
||||
|
||||
const results = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
|
||||
const results = await PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync(
|
||||
'has_a_dog = ?',
|
||||
[true],
|
||||
{
|
||||
limit: 2,
|
||||
offset: 1,
|
||||
orderBy: [
|
||||
@@ -357,25 +352,26 @@ describe('postgres entity integration', () => {
|
||||
order: OrderByOrdering.ASCENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']);
|
||||
|
||||
const resultsMultipleOrderBy = await PostgresTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
|
||||
orderBy: [
|
||||
{
|
||||
fieldName: 'hasADog',
|
||||
order: OrderByOrdering.ASCENDING,
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
order: OrderByOrdering.DESCENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
const resultsMultipleOrderBy = await PostgresTestEntity.loader(
|
||||
vc1
|
||||
).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
|
||||
orderBy: [
|
||||
{
|
||||
fieldName: 'hasADog',
|
||||
order: OrderByOrdering.ASCENDING,
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
order: OrderByOrdering.DESCENDING,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(resultsMultipleOrderBy).toHaveLength(3);
|
||||
expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']);
|
||||
@@ -395,9 +391,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeCreate');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeCreate')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeCreate')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -406,27 +400,21 @@ describe('postgres entity integration', () => {
|
||||
.enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterCreate');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterCreate')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCreate')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.creator(vc1).setField('name', 'beforeAll').enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeAll');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeAll')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterAll').enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterAll');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterAll')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -435,9 +423,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterCommit');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterCommit')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit')
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -458,9 +444,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeUpdate');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeUpdate')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeUpdate')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -469,9 +453,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterUpdate');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterUpdate')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterUpdate')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -480,9 +462,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeAll');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeAll')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -491,9 +471,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterAll');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterAll')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll')
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
@@ -502,9 +480,7 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value afterCommit');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterCommit')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit')
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -522,9 +498,7 @@ describe('postgres entity integration', () => {
|
||||
PostgresTriggerTestEntity.enforceDeleteAsync(entityBeforeDelete)
|
||||
).rejects.toThrowError('name cannot have value beforeDelete');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeDelete')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeDelete')
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
const entityAfterDelete = await PostgresTriggerTestEntity.creator(vc1)
|
||||
@@ -534,9 +508,7 @@ describe('postgres entity integration', () => {
|
||||
PostgresTriggerTestEntity.enforceDeleteAsync(entityAfterDelete)
|
||||
).rejects.toThrowError('name cannot have value afterDelete');
|
||||
await expect(
|
||||
PostgresTriggerTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'afterDelete')
|
||||
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterDelete')
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -553,9 +525,10 @@ describe('postgres entity integration', () => {
|
||||
.enforceCreateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
|
||||
await expect(
|
||||
PostgresValidatorTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
|
||||
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
'beforeCreateAndBeforeUpdate'
|
||||
)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -575,9 +548,10 @@ describe('postgres entity integration', () => {
|
||||
.enforceUpdateAsync()
|
||||
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
|
||||
await expect(
|
||||
PostgresValidatorTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
|
||||
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
'beforeCreateAndBeforeUpdate'
|
||||
)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -592,9 +566,10 @@ describe('postgres entity integration', () => {
|
||||
.enforceCreateAsync();
|
||||
await PostgresValidatorTestEntity.enforceDeleteAsync(entityToDelete);
|
||||
await expect(
|
||||
PostgresValidatorTestEntity.loader(vc1)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('name', 'shouldBeDeleted')
|
||||
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
|
||||
'name',
|
||||
'shouldBeDeleted'
|
||||
)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('postgres errors', () => {
|
||||
const vc2 = new ViewerContext(
|
||||
createKnexIntegrationTestEntityCompanionProvider(shortTimeoutKnexInstance)
|
||||
);
|
||||
await expect(ErrorsTestEntity.loader(vc2).enforcing().loadByIDAsync(1)).rejects.toThrow(
|
||||
await expect(ErrorsTestEntity.loader(vc2).loadByIDAsync(1)).rejects.toThrow(
|
||||
EntityDatabaseAdapterTransientError
|
||||
);
|
||||
await shortTimeoutKnexInstance.destroy();
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-example",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "An example integration of the @expo/entity framework",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
@@ -23,7 +23,7 @@
|
||||
"author": "Expo",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity": "^0.25.3",
|
||||
"apollo-server-koa": "^2.16.1",
|
||||
"graphql": "^15.3.0",
|
||||
"koa": "^2.13.0",
|
||||
|
||||
@@ -19,9 +19,10 @@ router.get('/', async (ctx: ExampleContext) => {
|
||||
const viewerContext = ctx.state.viewerContext;
|
||||
let notes: readonly NoteEntity[] = [];
|
||||
if (viewerContext.isUserViewerContext()) {
|
||||
notes = await NoteEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualingAsync('userID', viewerContext.userID);
|
||||
notes = await NoteEntity.loader(viewerContext).loadManyByFieldEqualingAsync(
|
||||
'userID',
|
||||
viewerContext.userID
|
||||
);
|
||||
}
|
||||
ctx.body = {
|
||||
notes: notes.map((note) => note.getAllFields()),
|
||||
|
||||
@@ -46,16 +46,14 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
|
||||
return viewerContext.userID;
|
||||
},
|
||||
async noteByID(_root, args, { viewerContext }) {
|
||||
return await NoteEntity.loader(viewerContext).enforcing().loadByIDAsync(args.id);
|
||||
return await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
|
||||
},
|
||||
} as IResolverObject<any, GraphqlContext>,
|
||||
|
||||
User: {
|
||||
id: (root) => root,
|
||||
async notes(root, _args, { viewerContext }) {
|
||||
return await NoteEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualingAsync('userID', root);
|
||||
return await NoteEntity.loader(viewerContext).loadManyByFieldEqualingAsync('userID', root);
|
||||
},
|
||||
} as IResolverObject<string, GraphqlContext>,
|
||||
|
||||
@@ -79,18 +77,14 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
|
||||
.enforceCreateAsync();
|
||||
},
|
||||
async updateNote(_root, args, { viewerContext }) {
|
||||
const existingNote = await NoteEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(args.id);
|
||||
const existingNote = await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
|
||||
return await NoteEntity.updater(existingNote)
|
||||
.setField('title', args.note.title)
|
||||
.setField('body', args.note.body)
|
||||
.enforceUpdateAsync();
|
||||
},
|
||||
async deleteNote(_root, args, { viewerContext }) {
|
||||
const existingNote = await NoteEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(args.id);
|
||||
const existingNote = await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
|
||||
await NoteEntity.enforceDeleteAsync(existingNote);
|
||||
return existingNote;
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-full-integration-tests",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Full redis and knex integration tests for the entity framework",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
@@ -27,8 +27,8 @@
|
||||
"@expo/entity-database-adapter-knex": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity-cache-adapter-redis": "^0.25.2",
|
||||
"@expo/entity-database-adapter-knex": "^0.25.2"
|
||||
"@expo/entity": "^0.25.3",
|
||||
"@expo/entity-cache-adapter-redis": "^0.25.3",
|
||||
"@expo/entity-database-adapter-knex": "^0.25.3"
|
||||
}
|
||||
}
|
||||
+8
-11
@@ -153,10 +153,8 @@ describe('Entity cache inconsistency', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
// put entities in cache and dataloader
|
||||
await TestEntity.loader(viewerContext).enforcing().loadByIDAsync(entity1.getID());
|
||||
await TestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('other_string', 'hello');
|
||||
await TestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
|
||||
await TestEntity.loader(viewerContext).loadByFieldEqualingAsync('other_string', 'hello');
|
||||
|
||||
let openBarrier1: () => void;
|
||||
const barrier1 = new Promise<void>((resolve) => {
|
||||
@@ -179,7 +177,7 @@ describe('Entity cache inconsistency', () => {
|
||||
const viewerContextInternal = new ViewerContext(
|
||||
createFullIntegrationTestEntityCompanionProvider(knexInstance, redisCacheAdapterContext)
|
||||
);
|
||||
await TestEntity.loader(viewerContextInternal).enforcing().loadByIDAsync(entity1.getID());
|
||||
await TestEntity.loader(viewerContextInternal).loadByIDAsync(entity1.getID());
|
||||
|
||||
openBarrier2!();
|
||||
})(),
|
||||
@@ -203,12 +201,11 @@ describe('Entity cache inconsistency', () => {
|
||||
createFullIntegrationTestEntityCompanionProvider(knexInstance, redisCacheAdapterContext)
|
||||
);
|
||||
|
||||
const loadedById = await TestEntity.loader(viewerContextLast)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1.getID());
|
||||
const loadedByField = await TestEntity.loader(viewerContextLast)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('other_string', 'hello');
|
||||
const loadedById = await TestEntity.loader(viewerContextLast).loadByIDAsync(entity1.getID());
|
||||
const loadedByField = await TestEntity.loader(viewerContextLast).loadByFieldEqualingAsync(
|
||||
'other_string',
|
||||
'hello'
|
||||
);
|
||||
|
||||
expect(loadedById.getField('third_string')).toEqual('updated');
|
||||
expect(loadedByField!.getField('third_string')).toEqual('updated');
|
||||
|
||||
+4
-6
@@ -87,22 +87,20 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('parent_id', parent.getID())
|
||||
ChildEntity.loader(viewerContext).loadByFieldEqualingAsync('parent_id', parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
await ParentEntity.enforceDeleteAsync(parent);
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.toBeNull();
|
||||
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
|
||||
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
+6
-6
@@ -224,18 +224,18 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
|
||||
if (edgeDeletionBehavior === EntityEdgeDeletionBehavior.SET_NULL) {
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(category1.getID())
|
||||
).resolves.toBeNull();
|
||||
const otherLoaded = await OtherEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDNullableAsync(other1.getID());
|
||||
const otherLoaded = await OtherEntity.loader(viewerContext).loadByIDNullableAsync(
|
||||
other1.getID()
|
||||
);
|
||||
expect(otherLoaded?.getField('parent_category_id')).toBeNull();
|
||||
} else {
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(category1.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
OtherEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(other1.getID())
|
||||
OtherEntity.loader(viewerContext).loadByIDNullableAsync(other1.getID())
|
||||
).resolves.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-ip-address-field",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "IP address EntityField definitions for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -28,7 +28,7 @@
|
||||
"ip-address": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity": "^0.25.3",
|
||||
"@types/jsbn": "^1.2.29"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-secondary-cache-local-memory",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Local memory secondary cache for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -31,8 +31,8 @@
|
||||
"@expo/entity-cache-adapter-local-memory": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity-cache-adapter-local-memory": "^0.25.2",
|
||||
"@expo/entity": "^0.25.3",
|
||||
"@expo/entity-cache-adapter-local-memory": "^0.25.3",
|
||||
"nullthrows": "^1.1.1"
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -159,11 +159,9 @@ class TestSecondaryLocalMemoryCacheLoader extends EntitySecondaryCacheLoader<
|
||||
}
|
||||
return nullthrows(
|
||||
(
|
||||
await this.entityLoader
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'id', fieldValue: loadParams.id },
|
||||
])
|
||||
await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'id', fieldValue: loadParams.id },
|
||||
])
|
||||
)[0]
|
||||
).getAllFields();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-secondary-cache-redis",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Redis secondary cache for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -34,8 +34,8 @@
|
||||
"ioredis": "^4.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.25.2",
|
||||
"@expo/entity-cache-adapter-redis": "^0.25.2",
|
||||
"@expo/entity": "^0.25.3",
|
||||
"@expo/entity-cache-adapter-redis": "^0.25.3",
|
||||
"nullthrows": "^1.1.1"
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -40,11 +40,9 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader<
|
||||
}
|
||||
return nullthrows(
|
||||
(
|
||||
await this.entityLoader
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'id', fieldValue: loadParams.id },
|
||||
])
|
||||
await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'id', fieldValue: loadParams.id },
|
||||
])
|
||||
)[0]
|
||||
).getAllFields();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.3](https://github.com/expo/entity/compare/v0.25.2...v0.25.3) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/expo/entity/compare/v0.25.1...v0.25.2) (2022-03-12)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "A privacy-first data model",
|
||||
"files": [
|
||||
"build",
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { FieldEqualityCondition, QuerySelectionModifiers } from './EntityDatabaseAdapter';
|
||||
import EntityLoader from './EntityLoader';
|
||||
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
|
||||
import ReadonlyEntity from './ReadonlyEntity';
|
||||
import ViewerContext from './ViewerContext';
|
||||
import { mapMap } from './utils/collections/maps';
|
||||
|
||||
/**
|
||||
* Enforcing view on an entity loader. All loads through this loader will throw
|
||||
* if the loads are not successful.
|
||||
*/
|
||||
export default class EnforcingEntityLoader<
|
||||
TFields,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TPrivacyPolicy extends EntityPrivacyPolicy<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
constructor(
|
||||
private readonly entityLoader: EntityLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly TEntity[]>> {
|
||||
const fieldValuesToResults = await this.entityLoader.loadManyByFieldEqualingManyAsync(
|
||||
fieldName,
|
||||
fieldValues
|
||||
);
|
||||
return mapMap(fieldValuesToResults, (results) =>
|
||||
results.map((result) => result.enforceValue())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.entityLoader.loadManyByFieldEqualingAsync(
|
||||
fieldName,
|
||||
fieldValue
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
|
||||
* @throws when multiple entities are found matching the condition
|
||||
*/
|
||||
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
uniqueFieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<TEntity | null> {
|
||||
const entityResult = await this.entityLoader.loadByFieldEqualingAsync(
|
||||
uniqueFieldName,
|
||||
fieldValue
|
||||
);
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
|
||||
*/
|
||||
async loadByIDAsync(id: TID): Promise<TEntity> {
|
||||
const entityResult = await this.entityLoader.loadByIDAsync(id);
|
||||
return entityResult.enforceValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
|
||||
* @throws when multiple entities are found matching the condition
|
||||
*/
|
||||
async loadByIDNullableAsync(id: TID): Promise<TEntity | null> {
|
||||
const entityResult = await this.entityLoader.loadByIDNullableAsync(id);
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, TEntity>> {
|
||||
const entityResults = await this.entityLoader.loadManyByIDsAsync(ids);
|
||||
return mapMap(entityResults, (result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.entityLoader.loadManyByFieldEqualityConjunctionAsync(
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByRawWhereClauseAsync(
|
||||
rawWhereClause: string,
|
||||
bindings: any[] | object,
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.entityLoader.loadManyByRawWhereClauseAsync(
|
||||
rawWhereClause,
|
||||
bindings,
|
||||
querySelectionModifiers
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,9 @@ export default class EntityAssociationLoader<
|
||||
.getLoaderFactory()
|
||||
.forLoad(queryContext, { cascadingDeleteCause: null });
|
||||
|
||||
return (await loader.loadByIDAsync(associatedEntityID as unknown as TAssociatedID)) as Result<
|
||||
return (await loader.resultLoader.loadByIDAsync(
|
||||
associatedEntityID as unknown as TAssociatedID
|
||||
)) as Result<
|
||||
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
|
||||
>;
|
||||
}
|
||||
@@ -124,7 +126,7 @@ export default class EntityAssociationLoader<
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
.getLoaderFactory()
|
||||
.forLoad(queryContext, { cascadingDeleteCause: null });
|
||||
return await loader.loadManyByFieldEqualingAsync(
|
||||
return await loader.resultLoader.loadManyByFieldEqualingAsync(
|
||||
associatedEntityFieldContainingThisID,
|
||||
thisID as any
|
||||
);
|
||||
@@ -181,7 +183,7 @@ export default class EntityAssociationLoader<
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
.getLoaderFactory()
|
||||
.forLoad(queryContext, { cascadingDeleteCause: null });
|
||||
return await loader.loadByFieldEqualingAsync(
|
||||
return await loader.resultLoader.loadByFieldEqualingAsync(
|
||||
associatedEntityLookupByField,
|
||||
associatedFieldValue as any
|
||||
);
|
||||
@@ -239,7 +241,7 @@ export default class EntityAssociationLoader<
|
||||
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
|
||||
.getLoaderFactory()
|
||||
.forLoad(queryContext, { cascadingDeleteCause: null });
|
||||
return await loader.loadManyByFieldEqualingAsync(
|
||||
return await loader.resultLoader.loadManyByFieldEqualingAsync(
|
||||
associatedEntityLookupByField,
|
||||
associatedFieldValue as any
|
||||
);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { Result, asyncResult, result } from '@expo/results';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import EnforcingEntityLoader from './EnforcingEntityLoader';
|
||||
import { IEntityClass } from './Entity';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import {
|
||||
FieldEqualityCondition,
|
||||
QuerySelectionModifiers,
|
||||
isSingleValueFieldEqualityCondition,
|
||||
} from './EntityDatabaseAdapter';
|
||||
import { FieldEqualityCondition, QuerySelectionModifiers } from './EntityDatabaseAdapter';
|
||||
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from './EntityQueryContext';
|
||||
import EntityResultLoader from './EntityResultLoader';
|
||||
import ReadonlyEntity from './ReadonlyEntity';
|
||||
import ViewerContext from './ViewerContext';
|
||||
import EntityInvalidFieldValueError from './errors/EntityInvalidFieldValueError';
|
||||
import EntityNotFoundError from './errors/EntityNotFoundError';
|
||||
import EntityDataManager from './internal/EntityDataManager';
|
||||
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
|
||||
import { mapMap, mapMapAsync } from './utils/collections/maps';
|
||||
import { mapMap } from './utils/collections/maps';
|
||||
|
||||
/**
|
||||
* The primary interface for loading entities. All normal loads are batched,
|
||||
@@ -37,12 +28,24 @@ export default class EntityLoader<
|
||||
>,
|
||||
TSelectedFields extends keyof TFields
|
||||
> {
|
||||
/**
|
||||
* Result-based view on an entity loader. All loads through this loader will wrap their results in Result.
|
||||
*/
|
||||
public readonly resultLoader: EntityResultLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>;
|
||||
|
||||
constructor(
|
||||
private readonly viewerContext: TViewerContext,
|
||||
private readonly queryContext: EntityQueryContext,
|
||||
private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly entityClass: IEntityClass<
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
@@ -50,133 +53,105 @@ export default class EntityLoader<
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly privacyPolicy: TPrivacyPolicy,
|
||||
privacyPolicy: TPrivacyPolicy,
|
||||
private readonly dataManager: EntityDataManager<TFields>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Enforcing view on this entity loader. All loads through this view are
|
||||
* guaranteed to be the values of successful results (or null for some loader methods),
|
||||
* and will throw otherwise.
|
||||
*/
|
||||
enforcing(): EnforcingEntityLoader<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
> {
|
||||
return new EnforcingEntityLoader(this);
|
||||
) {
|
||||
this.resultLoader = new EntityResultLoader(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
privacyPolicyEvaluationContext,
|
||||
entityConfiguration,
|
||||
entityClass,
|
||||
privacyPolicy,
|
||||
dataManager,
|
||||
metricsAdapter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entities where fieldName is one of fieldValues.
|
||||
* @param fieldName - entity field being queried
|
||||
* @param fieldValues - fieldName field values being queried
|
||||
* @returns map from fieldValue to entity results that match the query for that fieldValue,
|
||||
* where result errors can be UnauthorizedError
|
||||
* @returns map from fieldValue to entities that match the query for that fieldValue
|
||||
*/
|
||||
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
|
||||
this.validateFieldValues(fieldName, fieldValues);
|
||||
|
||||
const fieldValuesToFieldObjects = await this.dataManager.loadManyByFieldEqualingAsync(
|
||||
this.queryContext,
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly TEntity[]>> {
|
||||
const fieldValuesToResults = await this.resultLoader.loadManyByFieldEqualingManyAsync(
|
||||
fieldName,
|
||||
fieldValues
|
||||
);
|
||||
|
||||
return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects);
|
||||
return mapMap(fieldValuesToResults, (results) =>
|
||||
results.map((result) => result.enforceValue())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entities where fieldName equals fieldValue.
|
||||
* @param fieldName - entity field being queried
|
||||
* @param fieldValue - fieldName field value being queried
|
||||
* @returns array of entity results that match the query for fieldValue, where result error can be UnauthorizedError
|
||||
* @returns array of entities that match the query for fieldValue
|
||||
*/
|
||||
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const entityResults = await this.loadManyByFieldEqualingManyAsync(fieldName, [fieldValue]);
|
||||
const entityResultsForFieldValue = entityResults.get(fieldValue);
|
||||
invariant(
|
||||
entityResultsForFieldValue !== undefined,
|
||||
`${fieldValue} should be guaranteed to be present in returned map of entities`
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.resultLoader.loadManyByFieldEqualingAsync(
|
||||
fieldName,
|
||||
fieldValue
|
||||
);
|
||||
return entityResultsForFieldValue!;
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity where fieldName equals fieldValue, or null if no entity exists.
|
||||
* @param uniqueFieldName - entity field being queried
|
||||
* @param fieldValue - uniqueFieldName field value being queried
|
||||
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
|
||||
* @returns entity where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
|
||||
* @throws when multiple entities match the condition
|
||||
*/
|
||||
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
uniqueFieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<Result<TEntity> | null> {
|
||||
const entityResults = await this.loadManyByFieldEqualingAsync(uniqueFieldName, fieldValue);
|
||||
invariant(
|
||||
entityResults.length <= 1,
|
||||
`loadByFieldEqualing: Multiple entities of type ${this.entityClass.name} found for ${uniqueFieldName}=${fieldValue}`
|
||||
): Promise<TEntity | null> {
|
||||
const entityResult = await this.resultLoader.loadByFieldEqualingAsync(
|
||||
uniqueFieldName,
|
||||
fieldValue
|
||||
);
|
||||
return entityResults[0] ?? null;
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an entity by a specified ID.
|
||||
* @param id - ID of the entity
|
||||
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
* @returns entity for matching ID
|
||||
*/
|
||||
async loadByIDAsync(id: TID): Promise<Result<TEntity>> {
|
||||
const entityResults = await this.loadManyByIDsAsync([id]);
|
||||
const entityResult = entityResults.get(id);
|
||||
if (entityResult === undefined) {
|
||||
return result(
|
||||
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id)
|
||||
);
|
||||
}
|
||||
return entityResult;
|
||||
async loadByIDAsync(id: TID): Promise<TEntity> {
|
||||
const entityResult = await this.resultLoader.loadByIDAsync(id);
|
||||
return entityResult.enforceValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity by a specified ID, or return null if non-existent.
|
||||
* @param id - ID of the entity
|
||||
* @returns entity result for matching ID, or null if no entity exists for ID.
|
||||
* @returns entity 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 as TSelectedFields,
|
||||
id
|
||||
);
|
||||
async loadByIDNullableAsync(id: TID): Promise<TEntity | null> {
|
||||
const entityResult = await this.resultLoader.loadByIDNullableAsync(id);
|
||||
return entityResult ? entityResult.enforceValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads many entities for a list of IDs.
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param ids - IDs of the entities to load
|
||||
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
* @returns map from ID to corresponding entity
|
||||
*/
|
||||
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
|
||||
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
|
||||
this.entityConfiguration.idField as TSelectedFields,
|
||||
ids
|
||||
)) 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))
|
||||
);
|
||||
});
|
||||
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, TEntity>> {
|
||||
const entityResults = await this.resultLoader.loadManyByIDsAsync(ids);
|
||||
return mapMap(entityResults, (result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,41 +166,17 @@ export default class EntityLoader<
|
||||
*
|
||||
* @param fieldEqualityOperands - list of field equality WHERE clause operand specifications
|
||||
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
||||
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
||||
* @returns array of entities that match the query
|
||||
*/
|
||||
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
||||
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
||||
? [fieldEqualityOperand.fieldValue]
|
||||
: fieldEqualityOperand.fieldValues;
|
||||
this.validateFieldValues(fieldEqualityOperand.fieldName, fieldValues);
|
||||
}
|
||||
|
||||
const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.resultLoader.loadManyByFieldEqualityConjunctionAsync(
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers
|
||||
);
|
||||
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +199,7 @@ export default class EntityLoader<
|
||||
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
|
||||
* @param bindings - array of positional bindings or object of named bindings
|
||||
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
||||
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
||||
* @returns array of entities that match the query
|
||||
* @throws Error when rawWhereClause or bindings are invalid
|
||||
*
|
||||
* @deprecated prefer caching loaders
|
||||
@@ -257,30 +208,13 @@ export default class EntityLoader<
|
||||
rawWhereClause: string,
|
||||
bindings: any[] | object,
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const fieldObjects = await this.dataManager.loadManyByRawWhereClauseAsync(
|
||||
this.queryContext,
|
||||
): Promise<readonly TEntity[]> {
|
||||
const entityResults = await this.resultLoader.loadManyByRawWhereClauseAsync(
|
||||
rawWhereClause,
|
||||
bindings,
|
||||
querySelectionModifiers
|
||||
);
|
||||
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
return entityResults.map((result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,63 +233,4 @@ export default class EntityLoader<
|
||||
async invalidateEntityAsync(entity: TEntity): Promise<void> {
|
||||
await this.invalidateFieldsAsync(entity.getAllDatabaseFields());
|
||||
}
|
||||
|
||||
private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
|
||||
return fieldsObjects.map((fieldsObject) => {
|
||||
try {
|
||||
return result(new this.entityClass(this.viewerContext, fieldsObject));
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
throw e;
|
||||
}
|
||||
return result(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and authorize entities from fields map, returning error results for entities that fail
|
||||
* to construct or fail to authorize.
|
||||
*
|
||||
* @param map - map from an arbitrary key type to an array of entity field objects
|
||||
*/
|
||||
public async constructAndAuthorizeEntitiesAsync<K>(
|
||||
map: ReadonlyMap<K, readonly Readonly<TFields>[]>
|
||||
): Promise<ReadonlyMap<K, readonly Result<TEntity>[]>> {
|
||||
const uncheckedEntityResultsMap = mapMap(map, (fieldObjects) =>
|
||||
this.tryConstructEntities(fieldObjects)
|
||||
);
|
||||
return await mapMapAsync(uncheckedEntityResultsMap, async (uncheckedEntityResults) => {
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly TFields[N][]
|
||||
): void {
|
||||
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
|
||||
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
|
||||
for (const fieldValue of fieldValues) {
|
||||
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
|
||||
if (!isInputValid) {
|
||||
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,9 +261,7 @@ export class CreateMutator<
|
||||
);
|
||||
|
||||
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
|
||||
const newEntity = await entityLoader
|
||||
.enforcing()
|
||||
.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
|
||||
const newEntity = await entityLoader.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
|
||||
|
||||
await this.executeMutationTriggersAsync(
|
||||
this.mutationTriggers.afterCreate,
|
||||
@@ -469,9 +467,7 @@ export class UpdateMutator<
|
||||
);
|
||||
|
||||
const unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
|
||||
const updatedEntity = await entityLoader
|
||||
.enforcing()
|
||||
.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
|
||||
const updatedEntity = await entityLoader.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
|
||||
|
||||
await this.executeMutationTriggersAsync(
|
||||
this.mutationTriggers.afterUpdate,
|
||||
@@ -759,7 +755,7 @@ export class DeleteMutator<
|
||||
if (associatedEntityLookupByField) {
|
||||
inboundReferenceEntities = await loaderFactory
|
||||
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
|
||||
.enforcing()
|
||||
|
||||
.loadManyByFieldEqualingAsync(
|
||||
fieldName,
|
||||
entity.getField(associatedEntityLookupByField as any)
|
||||
@@ -767,7 +763,7 @@ export class DeleteMutator<
|
||||
} else {
|
||||
inboundReferenceEntities = await loaderFactory
|
||||
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
|
||||
.enforcing()
|
||||
|
||||
.loadManyByFieldEqualingAsync(fieldName, entity.getID());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import { Result, asyncResult, result } from '@expo/results';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { IEntityClass } from './Entity';
|
||||
import EntityConfiguration from './EntityConfiguration';
|
||||
import {
|
||||
FieldEqualityCondition,
|
||||
QuerySelectionModifiers,
|
||||
isSingleValueFieldEqualityCondition,
|
||||
} from './EntityDatabaseAdapter';
|
||||
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from './EntityQueryContext';
|
||||
import ReadonlyEntity from './ReadonlyEntity';
|
||||
import ViewerContext from './ViewerContext';
|
||||
import EntityInvalidFieldValueError from './errors/EntityInvalidFieldValueError';
|
||||
import EntityNotFoundError from './errors/EntityNotFoundError';
|
||||
import EntityDataManager from './internal/EntityDataManager';
|
||||
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
|
||||
import { mapMap, mapMapAsync } from './utils/collections/maps';
|
||||
|
||||
/**
|
||||
* Result-based view on an entity loader. All loads through this loader will wrap their results in Result.
|
||||
*/
|
||||
export default class EntityResultLoader<
|
||||
TFields,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
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 privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly entityClass: IEntityClass<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TPrivacyPolicy,
|
||||
TSelectedFields
|
||||
>,
|
||||
private readonly privacyPolicy: TPrivacyPolicy,
|
||||
private readonly dataManager: EntityDataManager<TFields>,
|
||||
protected readonly metricsAdapter: IEntityMetricsAdapter
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load many entity results where fieldName is one of fieldValues.
|
||||
* @param fieldName - entity field being queried
|
||||
* @param fieldValues - fieldName field values being queried
|
||||
* @returns map from fieldValue to entity results that match the query for that fieldValue,
|
||||
* where result errors can be UnauthorizedError
|
||||
*/
|
||||
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
|
||||
this.validateFieldValues(fieldName, fieldValues);
|
||||
|
||||
const fieldValuesToFieldObjects = await this.dataManager.loadManyByFieldEqualingAsync(
|
||||
this.queryContext,
|
||||
fieldName,
|
||||
fieldValues
|
||||
);
|
||||
|
||||
return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load many entity results where fieldName equals fieldValue.
|
||||
* @param fieldName - entity field being queried
|
||||
* @param fieldValue - fieldName field value being queried
|
||||
* @returns array of entity results that match the query for fieldValue, where result error can be UnauthorizedError
|
||||
*/
|
||||
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const entityResults = await this.loadManyByFieldEqualingManyAsync(fieldName, [fieldValue]);
|
||||
const entityResultsForFieldValue = entityResults.get(fieldValue);
|
||||
invariant(
|
||||
entityResultsForFieldValue !== undefined,
|
||||
`${fieldValue} should be guaranteed to be present in returned map of entities`
|
||||
);
|
||||
return entityResultsForFieldValue!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity result where fieldName equals fieldValue, or null if no entity exists.
|
||||
* @param uniqueFieldName - entity field being queried
|
||||
* @param fieldValue - uniqueFieldName field value being queried
|
||||
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
|
||||
* @throws when multiple entities match the condition
|
||||
*/
|
||||
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
uniqueFieldName: N,
|
||||
fieldValue: NonNullable<TFields[N]>
|
||||
): Promise<Result<TEntity> | null> {
|
||||
const entityResults = await this.loadManyByFieldEqualingAsync(uniqueFieldName, fieldValue);
|
||||
invariant(
|
||||
entityResults.length <= 1,
|
||||
`loadByFieldEqualing: Multiple entities of type ${this.entityClass.name} found for ${uniqueFieldName}=${fieldValue}`
|
||||
);
|
||||
return entityResults[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an entityresult by a specified ID.
|
||||
* @param id - ID of the entity
|
||||
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
*/
|
||||
async loadByIDAsync(id: TID): Promise<Result<TEntity>> {
|
||||
const entityResults = await this.loadManyByIDsAsync([id]);
|
||||
const entityResult = entityResults.get(id);
|
||||
if (entityResult === undefined) {
|
||||
return result(
|
||||
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id)
|
||||
);
|
||||
}
|
||||
return entityResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an entity result by a specified ID, or return null if non-existent.
|
||||
* @param id - ID of the entity
|
||||
* @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 as TSelectedFields,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads many entity results for a list of IDs.
|
||||
* @param viewerContext - viewer context of loading user
|
||||
* @param ids - IDs of the entities to load
|
||||
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
*/
|
||||
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
|
||||
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
|
||||
this.entityConfiguration.idField as TSelectedFields,
|
||||
ids
|
||||
)) 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))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads many entity results matching the conjunction of WHERE clauses constructed from specified operands.
|
||||
* Entities loaded using this method are not batched or cached.
|
||||
*
|
||||
* @example
|
||||
* fieldEqualityOperands:
|
||||
* `[{fieldName: 'hello', fieldValue: 1}, {fieldName: 'world', fieldValues: [2, 3]}]`
|
||||
* Entities returned:
|
||||
* `WHERE hello = 1 AND world = ANY({2, 3})`
|
||||
*
|
||||
* @param fieldEqualityOperands - list of field equality WHERE clause operand specifications
|
||||
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
||||
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
||||
*/
|
||||
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
||||
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
||||
? [fieldEqualityOperand.fieldValue]
|
||||
: fieldEqualityOperand.fieldValues;
|
||||
this.validateFieldValues(fieldEqualityOperand.fieldName, fieldValues);
|
||||
}
|
||||
|
||||
const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
|
||||
this.queryContext,
|
||||
fieldEqualityOperands,
|
||||
querySelectionModifiers
|
||||
);
|
||||
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads many entity results matching the raw WHERE clause. Corresponds to the knex `whereRaw` argument format.
|
||||
*
|
||||
* @remarks
|
||||
* Important notes:
|
||||
* - Fields in clause are database column names instead of transformed entity field names.
|
||||
* - Entities loaded using this method are not batched or cached.
|
||||
* - Not all database adapters implement the ability to execute this method of fetching entities.
|
||||
*
|
||||
* @example
|
||||
* rawWhereClause: `id = ?`
|
||||
* bindings: `[1]`
|
||||
* Entites returned `WHERE id = 1`
|
||||
*
|
||||
* {@link http://knexjs.org/#Builder-whereRaw}
|
||||
* {@link http://knexjs.org/#Raw-Bindings}
|
||||
*
|
||||
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
|
||||
* @param bindings - array of positional bindings or object of named bindings
|
||||
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
||||
* @returns array of entity results that match the query, where result error can be UnauthorizedError
|
||||
* @throws Error when rawWhereClause or bindings are invalid
|
||||
*
|
||||
* @deprecated prefer caching loaders
|
||||
*/
|
||||
async loadManyByRawWhereClauseAsync(
|
||||
rawWhereClause: string,
|
||||
bindings: any[] | object,
|
||||
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
|
||||
): Promise<readonly Result<TEntity>[]> {
|
||||
const fieldObjects = await this.dataManager.loadManyByRawWhereClauseAsync(
|
||||
this.queryContext,
|
||||
rawWhereClause,
|
||||
bindings,
|
||||
querySelectionModifiers
|
||||
);
|
||||
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
|
||||
return fieldsObjects.map((fieldsObject) => {
|
||||
try {
|
||||
return result(new this.entityClass(this.viewerContext, fieldsObject));
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
throw e;
|
||||
}
|
||||
return result(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and authorize entities from fields map, returning error results for entities that fail
|
||||
* to construct or fail to authorize.
|
||||
*
|
||||
* @param map - map from an arbitrary key type to an array of entity field objects
|
||||
*/
|
||||
public async constructAndAuthorizeEntitiesAsync<K>(
|
||||
map: ReadonlyMap<K, readonly Readonly<TFields>[]>
|
||||
): Promise<ReadonlyMap<K, readonly Result<TEntity>[]>> {
|
||||
const uncheckedEntityResultsMap = mapMap(map, (fieldObjects) =>
|
||||
this.tryConstructEntities(fieldObjects)
|
||||
);
|
||||
return await mapMapAsync(uncheckedEntityResultsMap, async (uncheckedEntityResults) => {
|
||||
return await Promise.all(
|
||||
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
|
||||
if (!uncheckedEntityResult.ok) {
|
||||
return uncheckedEntityResult;
|
||||
}
|
||||
return await asyncResult(
|
||||
this.privacyPolicy.authorizeReadAsync(
|
||||
this.viewerContext,
|
||||
this.queryContext,
|
||||
this.privacyPolicyEvaluationContext,
|
||||
uncheckedEntityResult.value,
|
||||
this.metricsAdapter
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
|
||||
fieldName: N,
|
||||
fieldValues: readonly TFields[N][]
|
||||
): void {
|
||||
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
|
||||
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
|
||||
for (const fieldValue of fieldValues) {
|
||||
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
|
||||
if (!isInputValid) {
|
||||
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { result } from '@expo/results';
|
||||
import { mock, instance, when, anything } from 'ts-mockito';
|
||||
|
||||
import EnforcingEntityLoader from '../EnforcingEntityLoader';
|
||||
import EntityLoader from '../EntityLoader';
|
||||
import EnforcingEntityLoader from '../EntityResultLoader';
|
||||
|
||||
describe(EnforcingEntityLoader, () => {
|
||||
describe('loadManyByFieldEqualingManyAsync', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualingManyAsync(anything(), anything())
|
||||
).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: [result(rejection)],
|
||||
@@ -26,7 +28,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualingManyAsync(anything(), anything())
|
||||
).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: [result(resolved)],
|
||||
@@ -51,9 +55,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
|
||||
result(rejection),
|
||||
]);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualingAsync(anything(), anything())
|
||||
).thenResolve([result(rejection)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -64,9 +68,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
|
||||
result(resolved),
|
||||
]);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualingAsync(anything(), anything())
|
||||
).thenResolve([result(resolved)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -79,9 +83,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(rejection)
|
||||
);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
|
||||
).thenResolve(result(rejection));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -92,9 +96,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(resolved)
|
||||
);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
|
||||
).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -105,9 +109,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns null when result is successful and no entity is found', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = null;
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
|
||||
result(resolved)
|
||||
);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
|
||||
).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -118,9 +122,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when multiple matching entities are found', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const multipleEntitiesError = new Error();
|
||||
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenReject(
|
||||
multipleEntitiesError
|
||||
);
|
||||
when(
|
||||
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
|
||||
).thenReject(multipleEntitiesError);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(
|
||||
@@ -133,7 +137,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(rejection));
|
||||
when(entityLoaderMock.resultLoader.loadByIDAsync(anything())).thenResolve(result(rejection));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadByIDAsync(anything())).rejects.toThrow(rejection);
|
||||
@@ -142,7 +146,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(resolved));
|
||||
when(entityLoaderMock.resultLoader.loadByIDAsync(anything())).thenResolve(result(resolved));
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadByIDAsync(anything())).resolves.toEqual(resolved);
|
||||
@@ -153,7 +157,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(rejection));
|
||||
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
|
||||
result(rejection)
|
||||
);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).rejects.toThrow(
|
||||
@@ -164,7 +170,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
|
||||
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
|
||||
result(resolved)
|
||||
);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).resolves.toEqual(
|
||||
@@ -175,7 +183,9 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns null when non-existent object', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = null;
|
||||
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
|
||||
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
|
||||
result(resolved)
|
||||
);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).resolves.toEqual(
|
||||
@@ -188,7 +198,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
|
||||
when(entityLoaderMock.resultLoader.loadManyByIDsAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: result(rejection),
|
||||
@@ -203,7 +213,7 @@ describe(EnforcingEntityLoader, () => {
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
|
||||
when(entityLoaderMock.resultLoader.loadManyByIDsAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: result(resolved),
|
||||
@@ -227,7 +237,10 @@ describe(EnforcingEntityLoader, () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(
|
||||
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualityConjunctionAsync(
|
||||
anything(),
|
||||
anything()
|
||||
)
|
||||
).thenResolve([result(rejection)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
@@ -240,7 +253,10 @@ describe(EnforcingEntityLoader, () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(
|
||||
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
|
||||
entityLoaderMock.resultLoader.loadManyByFieldEqualityConjunctionAsync(
|
||||
anything(),
|
||||
anything()
|
||||
)
|
||||
).thenResolve([result(resolved)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
@@ -255,7 +271,11 @@ describe(EnforcingEntityLoader, () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(
|
||||
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
|
||||
entityLoaderMock.resultLoader.loadManyByRawWhereClauseAsync(
|
||||
anything(),
|
||||
anything(),
|
||||
anything()
|
||||
)
|
||||
).thenResolve([result(rejection)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
@@ -268,7 +288,11 @@ describe(EnforcingEntityLoader, () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(
|
||||
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
|
||||
entityLoaderMock.resultLoader.loadManyByRawWhereClauseAsync(
|
||||
anything(),
|
||||
anything(),
|
||||
anything()
|
||||
)
|
||||
).thenResolve([result(resolved)]);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
|
||||
@@ -129,15 +129,15 @@ it('runs through a common workflow', async () => {
|
||||
|
||||
// check that two people can't read each others data
|
||||
await expect(
|
||||
enforceAsyncResult(BlahEntity.loader(vc1).loadByIDAsync(blahOwner2.getID()))
|
||||
enforceAsyncResult(BlahEntity.loader(vc1).resultLoader.loadByIDAsync(blahOwner2.getID()))
|
||||
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
||||
await expect(
|
||||
enforceAsyncResult(BlahEntity.loader(vc2).loadByIDAsync(blahOwner1.getID()))
|
||||
enforceAsyncResult(BlahEntity.loader(vc2).resultLoader.loadByIDAsync(blahOwner1.getID()))
|
||||
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
||||
|
||||
// check that all of owner 1's objects can be loaded
|
||||
const results = await enforceResultsAsync(
|
||||
BlahEntity.loader(vc1).loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!)
|
||||
BlahEntity.loader(vc1).resultLoader.loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!)
|
||||
);
|
||||
expect(results).toHaveLength(2);
|
||||
|
||||
@@ -149,7 +149,10 @@ it('runs through a common workflow', async () => {
|
||||
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
||||
|
||||
// check that empty load many returns nothing
|
||||
const results2 = await BlahEntity.loader(vc1).loadManyByFieldEqualingManyAsync('ownerID', []);
|
||||
const results2 = await BlahEntity.loader(vc1).resultLoader.loadManyByFieldEqualingManyAsync(
|
||||
'ownerID',
|
||||
[]
|
||||
);
|
||||
for (const value in results2.values) {
|
||||
expect(value).toHaveLength(0);
|
||||
}
|
||||
|
||||
@@ -374,13 +374,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
|
||||
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
||||
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
privacyPolicyEvaluationRecords.shouldRecord = true;
|
||||
@@ -388,13 +388,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
privacyPolicyEvaluationRecords.shouldRecord = false;
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
|
||||
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
||||
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
|
||||
).resolves.toBeNull();
|
||||
|
||||
// two calls for each trigger, one beforeDelete, one afterDelete
|
||||
@@ -488,13 +488,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
|
||||
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
||||
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
privacyPolicyEvaluationRecords.shouldRecord = true;
|
||||
@@ -502,16 +502,16 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
privacyPolicyEvaluationRecords.shouldRecord = false;
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.toBeNull();
|
||||
|
||||
const loadedChild = await ChildEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDAsync(child.getID());
|
||||
expect(loadedChild.getField('parent_id')).toBeNull();
|
||||
|
||||
const loadedGrandchild = await GrandChildEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDAsync(grandchild.getID());
|
||||
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
|
||||
|
||||
@@ -595,16 +595,16 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByFieldEqualingAsync('parent_id', parent.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
GrandChildEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByFieldEqualingAsync('parent_id', child.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
@@ -639,13 +639,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
||||
expect(grandChildCachedAfter.get(child.getID())?.status).toEqual(CacheStatus.MISS);
|
||||
|
||||
await expect(
|
||||
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
||||
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
|
||||
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
||||
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
// two calls for each trigger, one beforeDelete, one afterDelete
|
||||
|
||||
@@ -165,14 +165,14 @@ describe(EntityLoader, () => {
|
||||
|
||||
let capturedThrownThing1: any;
|
||||
try {
|
||||
await entityLoader.loadByIDAsync(ID_SENTINEL_THROW_LITERAL);
|
||||
await entityLoader.resultLoader.loadByIDAsync(ID_SENTINEL_THROW_LITERAL);
|
||||
} catch (e) {
|
||||
capturedThrownThing1 = e;
|
||||
}
|
||||
expect(capturedThrownThing1).not.toBeInstanceOf(Error);
|
||||
expect(capturedThrownThing1).toEqual('hello');
|
||||
|
||||
const result = await entityLoader.loadByIDAsync(ID_SENTINEL_THROW_ERROR);
|
||||
const result = await entityLoader.resultLoader.loadByIDAsync(ID_SENTINEL_THROW_ERROR);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.enforceError().message).toEqual('world');
|
||||
});
|
||||
|
||||
@@ -78,36 +78,39 @@ describe(EntityLoader, () => {
|
||||
dataManager,
|
||||
metricsAdapter
|
||||
);
|
||||
const entity = await enforceAsyncResult(entityLoader.loadByIDAsync(id1));
|
||||
const entity = await enforceAsyncResult(entityLoader.resultLoader.loadByIDAsync(id1));
|
||||
expect(entity.getID()).toEqual(id1);
|
||||
expect(entity.getField('dateField')).toEqual(dateToInsert);
|
||||
|
||||
const entities = await enforceResultsAsync(
|
||||
entityLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
entityLoader.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
);
|
||||
expect(entities.map((m) => m.getID())).toEqual([id1, id2]);
|
||||
|
||||
const entityResultNumber3 = await entityLoader.loadByFieldEqualingAsync('intField', 3);
|
||||
const entityResultNumber3 = await entityLoader.resultLoader.loadByFieldEqualingAsync('intField', 3);
|
||||
expect(entityResultNumber3).not.toBeNull();
|
||||
expect(entityResultNumber3!.enforceValue().getID()).toEqual(id2);
|
||||
|
||||
const entityResultNumber4 = await entityLoader.loadByFieldEqualingAsync('intField', 4);
|
||||
const entityResultNumber4 = await entityLoader.resultLoader.loadByFieldEqualingAsync('intField', 4);
|
||||
expect(entityResultNumber4).toBeNull();
|
||||
|
||||
const entityResultDuplicateValues = await entityLoader
|
||||
.enforcing()
|
||||
.loadManyByFieldEqualingManyAsync('stringField', ['huh', 'huh']);
|
||||
const entityResultDuplicateValues = await entityLoader.loadManyByFieldEqualingManyAsync(
|
||||
'stringField',
|
||||
['huh', 'huh']
|
||||
);
|
||||
expect(entityResultDuplicateValues.size).toBe(1);
|
||||
expect(entityResultDuplicateValues.get('huh')?.map((m) => m.getID())).toEqual([id1, id2]);
|
||||
|
||||
await expect(entityLoader.loadByFieldEqualingAsync('stringField', 'huh')).rejects.toThrowError(
|
||||
await expect(
|
||||
entityLoader.resultLoader.loadByFieldEqualingAsync('stringField', 'huh')
|
||||
).rejects.toThrowError(
|
||||
'loadByFieldEqualing: Multiple entities of type TestEntity found for stringField=huh'
|
||||
);
|
||||
|
||||
await expect(entityLoader.loadByIDNullableAsync(uuidv4())).resolves.toBeNull();
|
||||
await expect(entityLoader.loadByIDNullableAsync(id1)).resolves.not.toBeNull();
|
||||
await expect(entityLoader.resultLoader.loadByIDNullableAsync(uuidv4())).resolves.toBeNull();
|
||||
await expect(entityLoader.resultLoader.loadByIDNullableAsync(id1)).resolves.not.toBeNull();
|
||||
|
||||
await expect(entityLoader.loadByIDAsync('not-a-uuid')).rejects.toThrowError(
|
||||
await expect(entityLoader.resultLoader.loadByIDAsync('not-a-uuid')).rejects.toThrowError(
|
||||
'Entity field not valid: TestEntity (customIdField = not-a-uuid)'
|
||||
);
|
||||
});
|
||||
@@ -181,7 +184,7 @@ describe(EntityLoader, () => {
|
||||
metricsAdapter
|
||||
);
|
||||
const entities = await enforceResultsAsync(
|
||||
entityLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
entityLoader.resultLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
{
|
||||
fieldName: 'stringField',
|
||||
fieldValue: 'huh',
|
||||
@@ -204,7 +207,7 @@ describe(EntityLoader, () => {
|
||||
).twice();
|
||||
|
||||
await expect(
|
||||
entityLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
entityLoader.resultLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
{ fieldName: 'customIdField', fieldValue: 'not-a-uuid' },
|
||||
])
|
||||
).rejects.toThrowError('Entity field not valid: TestEntity (customIdField = not-a-uuid)');
|
||||
@@ -261,7 +264,7 @@ describe(EntityLoader, () => {
|
||||
dataManager,
|
||||
metricsAdapter
|
||||
);
|
||||
const entity = await enforceAsyncResult(entityLoader.loadByIDAsync(id1));
|
||||
const entity = await enforceAsyncResult(entityLoader.resultLoader.loadByIDAsync(id1));
|
||||
verify(
|
||||
spiedPrivacyPolicy.authorizeReadAsync(
|
||||
viewerContext,
|
||||
@@ -395,7 +398,7 @@ describe(EntityLoader, () => {
|
||||
metricsAdapter
|
||||
);
|
||||
|
||||
const entityResult = await entityLoader.loadByIDAsync(id1);
|
||||
const entityResult = await entityLoader.resultLoader.loadByIDAsync(id1);
|
||||
expect(entityResult.ok).toBe(false);
|
||||
expect(entityResult.reason).toEqual(rejectionError);
|
||||
expect(entityResult.value).toBe(undefined);
|
||||
@@ -430,21 +433,21 @@ describe(EntityLoader, () => {
|
||||
|
||||
const loadByValue = uuidv4();
|
||||
|
||||
await expect(entityLoader.resultLoader.loadByIDAsync(loadByValue)).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadByIDAsync(loadByValue)).rejects.toEqual(error);
|
||||
await expect(entityLoader.enforcing().loadByIDAsync(loadByValue)).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.resultLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.resultLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.resultLoader.loadManyByFieldEqualingAsync('customIdField', loadByValue)
|
||||
).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.loadManyByFieldEqualingAsync('customIdField', loadByValue)
|
||||
).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.enforcing().loadManyByFieldEqualingAsync('customIdField', loadByValue)
|
||||
entityLoader.resultLoader.loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
|
||||
).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
|
||||
).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.enforcing().loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
|
||||
).rejects.toEqual(error);
|
||||
});
|
||||
});
|
||||
@@ -78,7 +78,7 @@ class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutation
|
||||
): Promise<void> {
|
||||
if (mutationInfo.type === EntityMutationType.DELETE) {
|
||||
const entityLoaded = await BlahEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDNullableAsync(entity.getID());
|
||||
if (entityLoaded) {
|
||||
throw new Error(
|
||||
@@ -97,7 +97,7 @@ describe('EntityMutator', () => {
|
||||
// put it in cache
|
||||
const entity = await BlahEntity.creator(viewerContext).enforceCreateAsync();
|
||||
const entityLoaded = await BlahEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDAsync(entity.getID());
|
||||
|
||||
await BlahEntity.enforceDeleteAsync(entityLoaded);
|
||||
|
||||
@@ -530,7 +530,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id2)
|
||||
.resultLoader.loadByIDAsync(id2)
|
||||
);
|
||||
|
||||
const updatedEntity = await entityMutatorFactory
|
||||
@@ -545,7 +545,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const reloadedEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id2)
|
||||
.resultLoader.loadByIDAsync(id2)
|
||||
);
|
||||
expect(reloadedEntity.getAllFields()).toMatchObject(updatedEntity.getAllFields());
|
||||
});
|
||||
@@ -582,7 +582,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id2)
|
||||
.resultLoader.loadByIDAsync(id2)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -633,7 +633,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id2)
|
||||
.resultLoader.loadByIDAsync(id2)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -687,7 +687,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id2)
|
||||
.resultLoader.loadByIDAsync(id2)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -723,7 +723,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id1)
|
||||
.resultLoader.loadByIDAsync(id1)
|
||||
);
|
||||
expect(existingEntity).toBeTruthy();
|
||||
|
||||
@@ -735,7 +735,7 @@ describe(EntityMutatorFactory, () => {
|
||||
enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id1)
|
||||
.resultLoader.loadByIDAsync(id1)
|
||||
)
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
@@ -763,7 +763,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id1)
|
||||
.resultLoader.loadByIDAsync(id1)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -804,7 +804,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id1)
|
||||
.resultLoader.loadByIDAsync(id1)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -849,7 +849,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const existingEntity = await enforceAsyncResult(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadByIDAsync(id1)
|
||||
.resultLoader.loadByIDAsync(id1)
|
||||
);
|
||||
|
||||
await entityMutatorFactory
|
||||
@@ -882,7 +882,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const entites1 = await enforceResultsAsync(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
);
|
||||
expect(entites1).toHaveLength(1);
|
||||
|
||||
@@ -896,7 +896,7 @@ describe(EntityMutatorFactory, () => {
|
||||
const entities2 = await enforceResultsAsync(
|
||||
entityLoaderFactory
|
||||
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
|
||||
.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
|
||||
);
|
||||
expect(entities2).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -98,25 +98,25 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
await CategoryEntity.enforceDeleteAsync(parentCategory);
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
@@ -137,10 +137,10 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
|
||||
await CategoryEntity.enforceDeleteAsync(categoryA);
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryA.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(categoryA.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryB.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(categoryB.getID())
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -161,29 +161,29 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
await CategoryEntity.enforceDeleteAsync(parentCategory);
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.toBeNull();
|
||||
|
||||
const loadedSubCategory = await CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(subCategory.getID());
|
||||
const loadedSubCategory = await CategoryEntity.loader(viewerContext).loadByIDAsync(
|
||||
subCategory.getID()
|
||||
);
|
||||
expect(loadedSubCategory.getField('parent_category_id')).toBeNull();
|
||||
|
||||
const loadedSubSubCategory = await CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(subSubCategory.getID());
|
||||
const loadedSubSubCategory = await CategoryEntity.loader(viewerContext).loadByIDAsync(
|
||||
subSubCategory.getID()
|
||||
);
|
||||
expect(loadedSubSubCategory.getField('parent_category_id')).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -203,9 +203,9 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
|
||||
|
||||
await CategoryEntity.enforceDeleteAsync(categoryA);
|
||||
|
||||
const loadedCategoryB = await CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(categoryB.getID());
|
||||
const loadedCategoryB = await CategoryEntity.loader(viewerContext).loadByIDAsync(
|
||||
categoryB.getID()
|
||||
);
|
||||
expect(loadedCategoryB.getField('parent_category_id')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -228,17 +228,19 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('parent_category_id', parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'parent_category_id',
|
||||
parentCategory.getID()
|
||||
)
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('parent_category_id', subCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'parent_category_id',
|
||||
subCategory.getID()
|
||||
)
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
const categoryCacheAdapter = viewerContext.getViewerScopedEntityCompanionForClass(
|
||||
@@ -271,13 +273,13 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
|
||||
expect(subSubCategoryCachedAfter.get(subCategory.getID())?.status).toEqual(CacheStatus.MISS);
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -298,14 +300,16 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
|
||||
.enforceUpdateAsync();
|
||||
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('parent_category_id', categoryA.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'parent_category_id',
|
||||
categoryA.getID()
|
||||
)
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
CategoryEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByFieldEqualingAsync('parent_category_id', categoryB.getID())
|
||||
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
||||
'parent_category_id',
|
||||
categoryB.getID()
|
||||
)
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
const categoryCacheAdapter = viewerContext.getViewerScopedEntityCompanionForClass(
|
||||
|
||||
@@ -28,17 +28,16 @@ describe('Two entities backed by the same table', () => {
|
||||
expect(two).toBeInstanceOf(TwoTestEntity);
|
||||
|
||||
await expect(
|
||||
TwoTestEntity.loader(viewerContext).enforcing().loadByIDAsync(one.getID())
|
||||
TwoTestEntity.loader(viewerContext).loadByIDAsync(one.getID())
|
||||
).rejects.toThrowError('TwoTestEntity must be instantiated with two data');
|
||||
|
||||
await expect(
|
||||
OneTestEntity.loader(viewerContext).enforcing().loadByIDAsync(two.getID())
|
||||
OneTestEntity.loader(viewerContext).loadByIDAsync(two.getID())
|
||||
).rejects.toThrowError('OneTestEntity must be instantiated with one data');
|
||||
|
||||
const manyResults = await OneTestEntity.loader(viewerContext).loadManyByFieldEqualingAsync(
|
||||
'common_other_field',
|
||||
'wat'
|
||||
);
|
||||
const manyResults = await OneTestEntity.loader(
|
||||
viewerContext
|
||||
).resultLoader.loadManyByFieldEqualingAsync('common_other_field', 'wat');
|
||||
const successfulManyResults = successfulResults(manyResults);
|
||||
const failedManyResults = failedResults(manyResults);
|
||||
|
||||
@@ -52,7 +51,7 @@ describe('Two entities backed by the same table', () => {
|
||||
|
||||
const fieldEqualityConjunctionResults = await OneTestEntity.loader(
|
||||
viewerContext
|
||||
).loadManyByFieldEqualityConjunctionAsync([
|
||||
).resultLoader.loadManyByFieldEqualityConjunctionAsync([
|
||||
{
|
||||
fieldName: 'common_other_field',
|
||||
fieldValue: 'wat',
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Two entities backed by the same table', () => {
|
||||
expect(entity1).toBeInstanceOf(OneTestEntity);
|
||||
|
||||
const entity2 = await TwoTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDAsync(entity1.getID());
|
||||
expect(entity2).toBeInstanceOf(TwoTestEntity);
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Two entities backed by the same table', () => {
|
||||
});
|
||||
|
||||
const loaded1 = await OneTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByIDAsync(entity1.getID());
|
||||
expect(loaded1.getAllFields()).toMatchObject({
|
||||
id: updated2.getID(),
|
||||
@@ -51,7 +51,7 @@ describe('Two entities backed by the same table', () => {
|
||||
.enforceCreateAsync();
|
||||
|
||||
const loadedEntity = await TwoTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByFieldEqualingAsync('other_field', 'huh');
|
||||
expect(loadedEntity?.getAllFields()).toMatchObject({
|
||||
id: entity.getID(),
|
||||
@@ -60,12 +60,12 @@ describe('Two entities backed by the same table', () => {
|
||||
});
|
||||
|
||||
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(),
|
||||
@@ -74,7 +74,7 @@ describe('Two entities backed by the same table', () => {
|
||||
});
|
||||
|
||||
const loaded22 = await TwoTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
|
||||
.loadByFieldEqualingAsync('fake_field', 'world');
|
||||
expect(loaded22?.getAllFields()).toMatchObject({
|
||||
id: entity.getID(),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export { default as ComposedEntityCacheAdapter } from './ComposedEntityCacheAdapter';
|
||||
export { default as ComposedSecondaryEntityCache } from './ComposedSecondaryEntityCache';
|
||||
export { default as GenericSecondaryEntityCache } from './GenericSecondaryEntityCache';
|
||||
export { default as EnforcingEntityLoader } from './EnforcingEntityLoader';
|
||||
export { default as EntityResultLoader } from './EntityResultLoader';
|
||||
export { default as Entity } from './Entity';
|
||||
export * from './Entity';
|
||||
export { default as EntityAssociationLoader } from './EntityAssociationLoader';
|
||||
|
||||
@@ -508,35 +508,35 @@
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@expo/entity-cache-adapter-local-memory@file:packages/entity-cache-adapter-local-memory":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
lru-cache "^7.3.1"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
ioredis "^4.27.3"
|
||||
|
||||
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
knex "^1.0.2"
|
||||
|
||||
"@expo/entity-ip-address-field@file:packages/entity-ip-address-field":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
ip-address "^8.1.0"
|
||||
|
||||
"@expo/entity-secondary-cache-local-memory@file:packages/entity-secondary-cache-local-memory":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
|
||||
"@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
ioredis "^4.17.3"
|
||||
|
||||
"@expo/entity@file:packages/entity":
|
||||
version "0.25.1"
|
||||
version "0.25.2"
|
||||
dependencies:
|
||||
"@expo/results" "^1.0.0"
|
||||
dataloader "^2.0.0"
|
||||
@@ -2003,10 +2003,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||
|
||||
"@types/lru-cache@^7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-7.4.0.tgz#74e50606962cb90448d928d6f0c7c872592fc84d"
|
||||
integrity sha512-jZ/Tb2/3vXw4VYd9AImFC/n6XT3WywZNAxwY8Ox9eM87M9ta9G7KzCx4aKo2Zllvr02k40eY328cjOO3WuK5Kw==
|
||||
"@types/lru-cache@^5.1.1":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
|
||||
integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
|
||||
|
||||
"@types/mime@*":
|
||||
version "2.0.1"
|
||||
@@ -6578,11 +6578,6 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^7.3.1:
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.4.4.tgz#a3dabc394ec07e2285af52fd24d0d74b3ac71c29"
|
||||
integrity sha512-2XbUJmlpIbmc9JvNNmtLzHlF31srxoDxuiQiwBHic7RZyHyltbTdzoO6maRqpdEhOOG5GD80EXvzAU0wR15ccg==
|
||||
|
||||
lunr@^2.3.9:
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
|
||||
|
||||
Reference in new issue
Block a user