Compare commits

...
Author SHA1 Message Date
Quinlan Jung a38e77e9af remove transformer test in read thru 2022-02-03 21:05:37 -08:00
Quinlan Jung 9ba1d430e0 [entity][entity-cache-adapter-redis] move transformer responsibility to cache adapter 2022-02-03 21:02:46 -08:00
James Ide a2c6dd7488 v0.22.0 (#151) 2022-02-03 18:52:25 -08:00
James Ide e1b0ee5281 feat: Upgrade knex to 1.x in @expo/entity-database-adapter-knex (#150)
Upgrades knex to 1.x internally. This changes the behavior of `returning` but not in a way that affects how the knex adapter uses it: `returning('*')` behaves the same as before.

This does, however, change the TypeScript declarations for knex, which changed from:
```
 Knex<TRecord extends {} = any, TResult = unknown[]>
```
to:
```
 Knex<TRecord extends {} = any, TResult = Record<string, any>[]>
```
2022-02-03 18:35:55 -08:00
Will Schurman 5d39fa928f v0.21.0 (#148) 2022-01-03 11:04:02 -08:00
Will Schurman f1d9847210 feat: add pre-commit callbacks on EntityQueryContext (#147) 2022-01-03 09:37:19 -08:00
Will Schurman 99657196f8 v0.20.0 (#146) 2021-12-29 10:50:58 -08:00
Will Schurman 3727191767 feat: add cascading deletion info (#145) 2021-12-29 10:39:22 -08:00
Will Schurman efa131ef29 fix: separate post-commit invalidation callbacks from trigger and user callbacks (#144) 2021-12-23 12:41:29 -08:00
36 changed files with 1015 additions and 244 deletions

No files matched your search

+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
### Features
* Upgrade knex to 1.x in `@expo/entity-database-adapter-knex` ([#150](https://github.com/expo/entity/issues/150)) ([e1b0ee5](https://github.com/expo/entity/commit/e1b0ee528175646239d88aa71ffce73983eeb52f))
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
### Features
* add pre-commit callbacks on EntityQueryContext ([#147](https://github.com/expo/entity/issues/147)) ([f1d9847](https://github.com/expo/entity/commit/f1d9847210f5775de964509e44ce493e44482b21))
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
### Bug Fixes
* separate post-commit invalidation callbacks from trigger and user callbacks ([#144](https://github.com/expo/entity/issues/144)) ([efa131e](https://github.com/expo/entity/commit/efa131ef29532accf0e51b6d867ffe15a27b4c90))
### Features
* add cascading deletion info ([#145](https://github.com/expo/entity/issues/145)) ([3727191](https://github.com/expo/entity/commit/372719176742d3ead6b7cbca29cb66b09b3fe09c))
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
+1 -1
View File
@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.19.0"
"version": "0.22.0"
}
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.19.0",
"version": "0.22.0",
"description": "Redis cache adapter for @expo/entity",
"files": [
"build",
@@ -33,6 +33,6 @@
"ioredis": "^4.27.3"
},
"devDependencies": {
"@expo/entity": "^0.19.0"
"@expo/entity": "^0.22.0"
}
}
@@ -4,6 +4,9 @@ import {
CacheLoadResult,
FieldTransformerMap,
mapKeys,
transformCacheObjectToFields,
CacheStatus,
transformFieldsToCacheObject,
} from '@expo/entity';
import invariant from 'invariant';
import { Redis } from 'ioredis';
@@ -49,6 +52,12 @@ export interface RedisCacheAdapterContext {
}
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
/**
* Transformer definitions for field types. Used to modify values as they are read from or written to
* the cache. Override in concrete subclasses to change transformation behavior.
* If a field type is not present in the map, then fields of that type will not be transformed.
*/
private readonly redisFieldTransformer: FieldTransformerMap = redisTransformerMap;
private readonly genericRedisCacher: GenericRedisCacher;
constructor(
entityConfiguration: EntityConfiguration<TFields>,
@@ -63,10 +72,6 @@ export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFiel
});
}
public getFieldTransformerMap(): FieldTransformerMap {
return redisTransformerMap;
}
public async loadManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
@@ -74,9 +79,24 @@ export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFiel
const redisCacheKeyToFieldValueMapping = new Map(
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
);
const cacheResults = await this.genericRedisCacher.loadManyAsync(
const rawCacheResults = await this.genericRedisCacher.loadManyAsync(
Array.from(redisCacheKeyToFieldValueMapping.keys())
);
const cacheResults = new Map<string, CacheLoadResult>();
for (const [redisCacheKey, rawCacheResult] of rawCacheResults) {
if (rawCacheResult.status === CacheStatus.HIT) {
cacheResults.set(redisCacheKey, {
status: CacheStatus.HIT,
item: transformCacheObjectToFields(
this.entityConfiguration,
this.redisFieldTransformer,
rawCacheResult.item
),
});
} else {
cacheResults.set(redisCacheKey, rawCacheResult);
}
}
return mapKeys(cacheResults, (redisCacheKey) => {
const fieldValue = redisCacheKeyToFieldValueMapping.get(redisCacheKey);
@@ -93,8 +113,19 @@ export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFiel
fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
): Promise<void> {
const cacheObjectMap = new Map<NonNullable<TFields[N]>, object>();
for (const [fieldValue, object] of objectMap) {
cacheObjectMap.set(
fieldValue,
transformFieldsToCacheObject(
this.entityConfiguration,
this.redisFieldTransformer,
object as Readonly<TFields>
)
);
}
await this.genericRedisCacher.cacheManyAsync(
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
mapKeys(cacheObjectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
@@ -1,4 +1,4 @@
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
import { CacheStatus, UUIDField, EntityConfiguration, DateField } from '@expo/entity';
import { Redis, Pipeline } from 'ioredis';
import { mock, when, instance, anything, verify } from 'ts-mockito';
@@ -6,6 +6,7 @@ import RedisCacheAdapter from '../RedisCacheAdapter';
type BlahFields = {
id: string;
date: Date | null;
};
const entityConfiguration = new EntityConfiguration<BlahFields>({
@@ -13,6 +14,7 @@ const entityConfiguration = new EntityConfiguration<BlahFields>({
tableName: 'blah',
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
date: new DateField({ columnName: 'date' }),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
@@ -62,6 +64,40 @@ describe(RedisCacheAdapter, () => {
const results = await cacheAdapter.loadManyAsync('id', []);
expect(results).toEqual(new Map());
});
it('transforms a cache object to an Entity object', async () => {
const redisResults = new Map();
const mockRedisClient = mock<Redis>();
// need to have one anything() for each element of spread, in this case 1
when(mockRedisClient.mget(anything())).thenCall(async (...keys) =>
keys.map((k) => redisResults.get(k) ?? null)
);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
const date = new Date();
redisResults.set(
cacheAdapter['makeCacheKey']('id', 'wat'),
JSON.stringify({ id: 'wat', date: date.toISOString() })
);
const results = await cacheAdapter.loadManyAsync('id', ['wat']);
expect(results.get('wat')).toMatchObject({
status: CacheStatus.HIT,
item: { id: 'wat', date },
});
expect(results.size).toBe(1);
});
});
describe('cacheManyAsync', () => {
@@ -98,6 +134,41 @@ describe(RedisCacheAdapter, () => {
ttl: 1,
});
});
it('transforms an Entity object to a cache object', async () => {
const redisResults = new Map();
const mockPipeline = mock<Pipeline>();
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
(key, value, code, ttl) => {
redisResults.set(key, { value, code, ttl });
return pipeline;
}
);
when(mockPipeline.exec()).thenResolve({} as any);
const pipeline = instance(mockPipeline);
const mockRedisClient = mock<Redis>();
when(mockRedisClient.multi()).thenReturn(pipeline);
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
redisClient: instance(mockRedisClient),
makeKeyFn: (...parts) => parts.join(':'),
cacheKeyVersion: 1,
cacheKeyPrefix: 'hello-',
ttlSecondsPositive: 1,
ttlSecondsNegative: 2,
});
const date = new Date();
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat', date }]]));
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
expect(redisResults.get(cacheKey)).toMatchObject({
value: JSON.stringify({ id: 'wat', date: date.toISOString() }),
code: 'EX',
ttl: 1,
});
});
});
describe('cacheDBMissesAsync', () => {
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
### Features
* Upgrade knex to 1.x in `@expo/entity-database-adapter-knex` ([#150](https://github.com/expo/entity/issues/150)) ([e1b0ee5](https://github.com/expo/entity/commit/e1b0ee528175646239d88aa71ffce73983eeb52f))
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
### Features
* add pre-commit callbacks on EntityQueryContext ([#147](https://github.com/expo/entity/issues/147)) ([f1d9847](https://github.com/expo/entity/commit/f1d9847210f5775de964509e44ce493e44482b21))
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
### Features
* add cascading deletion info ([#145](https://github.com/expo/entity/issues/145)) ([3727191](https://github.com/expo/entity/commit/372719176742d3ead6b7cbca29cb66b09b3fe09c))
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.19.0",
"version": "0.22.0",
"description": "Knex database adapter for @expo/entity",
"files": [
"build",
@@ -30,9 +30,10 @@
"@expo/entity": "*"
},
"dependencies": {
"knex": "^0.95.6"
"knex": "^1.0.2"
},
"devDependencies": {
"@expo/entity": "^0.19.0"
"@expo/entity": "^0.22.0",
"pg": "^8.7.1"
}
}
@@ -600,4 +600,37 @@ describe('postgres entity integration', () => {
});
});
});
describe('queryContext callback behavior', () => {
it('calls callbacks correctly', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
let preCommitCallCount = 0;
let postCommitCallCount = 0;
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
queryContext.appendPostCommitCallback(async () => {
postCommitCallCount++;
});
queryContext.appendPreCommitCallback(async () => {
preCommitCallCount++;
}, 0);
});
await expect(
vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
queryContext.appendPostCommitCallback(async () => {
postCommitCallCount++;
});
queryContext.appendPreCommitCallback(async () => {
preCommitCallCount++;
throw Error('wat');
}, 0);
})
).rejects.toThrowError('wat');
expect(preCommitCallCount).toBe(2);
expect(postCommitCallCount).toBe(1);
});
});
});
@@ -10,7 +10,7 @@ import {
EntityMutationTrigger,
EntityQueryContext,
EntityNonTransactionalMutationTrigger,
EntityMutationInfo,
EntityTriggerMutationInfo,
} from '@expo/entity';
import { Knex } from 'knex';
@@ -111,7 +111,7 @@ class ThrowConditionallyTrigger extends EntityMutationTrigger<
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
entity: PostgresTriggerTestEntity,
_mutationInfo: EntityMutationInfo<
_mutationInfo: EntityTriggerMutationInfo<
PostgresTriggerTestEntityFields,
string,
ViewerContext,
@@ -9,7 +9,7 @@ import {
Entity,
EntityMutationTrigger,
EntityQueryContext,
EntityMutationInfo,
EntityValidatorMutationInfo,
} from '@expo/entity';
import { Knex } from 'knex';
@@ -113,7 +113,7 @@ class ThrowConditionallyTrigger extends EntityMutationTrigger<
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
entity: PostgresValidatorTestEntity,
_mutationInfo: EntityMutationInfo<
_mutationInfo: EntityValidatorMutationInfo<
PostgresValidatorTestEntityFields,
string,
ViewerContext,
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-example
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-example
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-example
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-example
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-example",
"private": true,
"version": "0.19.0",
"version": "0.22.0",
"description": "An example integration of the @expo/entity framework",
"scripts": {
"tsc": "tsc",
@@ -23,7 +23,7 @@
"author": "Expo",
"license": "MIT",
"dependencies": {
"@expo/entity": "^0.19.0",
"@expo/entity": "^0.22.0",
"apollo-server-koa": "^2.16.1",
"graphql": "^15.3.0",
"koa": "^2.13.0",
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**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.19.0",
"version": "0.22.0",
"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.19.0",
"@expo/entity-cache-adapter-redis": "^0.19.0",
"@expo/entity-database-adapter-knex": "^0.19.0"
"@expo/entity": "^0.22.0",
"@expo/entity-cache-adapter-redis": "^0.22.0",
"@expo/entity-database-adapter-knex": "^0.22.0"
}
}
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-ip-address-field",
"version": "0.19.0",
"version": "0.22.0",
"description": "IP address EntityField definitions for @expo/entity",
"files": [
"build",
@@ -28,7 +28,7 @@
"ip-address": "^8.1.0"
},
"devDependencies": {
"@expo/entity": "^0.19.0",
"@expo/entity": "^0.22.0",
"@types/jsbn": "^1.2.29"
},
"peerDependencies": {
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-redis",
"version": "0.19.0",
"version": "0.22.0",
"description": "Redis secondary cache for @expo/entity",
"files": [
"build",
@@ -34,8 +34,8 @@
"ioredis": "^4.17.3"
},
"devDependencies": {
"@expo/entity": "^0.19.0",
"@expo/entity-cache-adapter-redis": "^0.19.0",
"@expo/entity": "^0.22.0",
"@expo/entity-cache-adapter-redis": "^0.22.0",
"nullthrows": "^1.1.1"
}
}
+35
View File
@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
**Note:** Version bump only for package @expo/entity
# [0.21.0](https://github.com/expo/entity/compare/v0.20.0...v0.21.0) (2022-01-03)
### Features
* add pre-commit callbacks on EntityQueryContext ([#147](https://github.com/expo/entity/issues/147)) ([f1d9847](https://github.com/expo/entity/commit/f1d9847210f5775de964509e44ce493e44482b21))
# [0.20.0](https://github.com/expo/entity/compare/v0.19.0...v0.20.0) (2021-12-29)
### Bug Fixes
* separate post-commit invalidation callbacks from trigger and user callbacks ([#144](https://github.com/expo/entity/issues/144)) ([efa131e](https://github.com/expo/entity/commit/efa131ef29532accf0e51b6d867ffe15a27b4c90))
### Features
* add cascading deletion info ([#145](https://github.com/expo/entity/issues/145)) ([3727191](https://github.com/expo/entity/commit/372719176742d3ead6b7cbca29cb66b09b3fe09c))
# [0.19.0](https://github.com/expo/entity/compare/v0.18.0...v0.19.0) (2021-10-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.19.0",
"version": "0.22.0",
"description": "A privacy-first data model",
"files": [
"build",
@@ -1,5 +1,4 @@
import EntityConfiguration from './EntityConfiguration';
import { FieldTransformerMap } from './internal/EntityFieldTransformationUtils';
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
/**
@@ -9,13 +8,6 @@ import { CacheLoadResult } from './internal/ReadThroughEntityCache';
export default abstract class EntityCacheAdapter<TFields> {
constructor(protected readonly entityConfiguration: EntityConfiguration<TFields>) {}
/**
* Transformer definitions for field types. Used to modify values as they are read from or written to
* the cache. Override in concrete subclasses to change transformation behavior.
* If a field type is not present in the map, then fields of that type will not be transformed.
*/
public abstract getFieldTransformerMap(): FieldTransformerMap;
/**
* Load many objects from cache.
* @param fieldName - object field being queried
+47
View File
@@ -0,0 +1,47 @@
import Entity from './Entity';
import ViewerContext from './ViewerContext';
export enum EntityMutationType {
CREATE,
UPDATE,
DELETE,
}
export type EntityValidatorMutationInfo<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> =
| {
type: EntityMutationType.CREATE;
}
| {
type: EntityMutationType.UPDATE;
previousValue: TEntity;
};
export type EntityMutationTriggerDeleteCascadeInfo<> = {
entity: Entity<any, any, any, any>;
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null;
};
export type EntityTriggerMutationInfo<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> =
| {
type: EntityMutationType.CREATE;
}
| {
type: EntityMutationType.UPDATE;
previousValue: TEntity;
}
| {
type: EntityMutationType.DELETE;
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null;
};
@@ -1,4 +1,4 @@
import { EntityMutationInfo } from './EntityMutator';
import { EntityTriggerMutationInfo } from './EntityMutationInfo';
import { EntityTransactionalQueryContext } from './EntityQueryContext';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
@@ -80,7 +80,7 @@ export abstract class EntityMutationTrigger<
viewerContext: TViewerContext,
queryContext: EntityTransactionalQueryContext,
entity: TEntity,
mutationInfo: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
): Promise<void>;
}
@@ -98,6 +98,6 @@ export abstract class EntityNonTransactionalMutationTrigger<
abstract executeAsync(
viewerContext: TViewerContext,
entity: TEntity,
mutationInfo: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
): Promise<void>;
}
@@ -1,4 +1,4 @@
import { EntityMutationInfo } from './EntityMutator';
import { EntityValidatorMutationInfo } from './EntityMutationInfo';
import { EntityTransactionalQueryContext } from './EntityQueryContext';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
@@ -18,6 +18,12 @@ export default abstract class EntityMutationValidator<
viewerContext: TViewerContext,
queryContext: EntityTransactionalQueryContext,
entity: TEntity,
mutationInfo: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
mutationInfo: EntityValidatorMutationInfo<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>
): Promise<void>;
}
+85 -72
View File
@@ -7,6 +7,12 @@ import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
import { EntityEdgeDeletionBehavior } from './EntityFieldDefinition';
import EntityLoaderFactory from './EntityLoaderFactory';
import {
EntityValidatorMutationInfo,
EntityMutationType,
EntityTriggerMutationInfo,
EntityMutationTriggerDeleteCascadeInfo,
} from './EntityMutationInfo';
import EntityMutationTriggerConfiguration, {
EntityMutationTrigger,
EntityNonTransactionalMutationTrigger,
@@ -21,30 +27,6 @@ import { timeAndLogMutationEventAsync } from './metrics/EntityMetricsUtils';
import IEntityMetricsAdapter, { EntityMetricsMutationType } from './metrics/IEntityMetricsAdapter';
import { mapMapAsync } from './utils/collections/maps';
export enum EntityMutationType {
CREATE,
UPDATE,
DELETE,
}
export type EntityMutationInfo<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> =
| {
type: EntityMutationType.CREATE;
}
| {
type: EntityMutationType.UPDATE;
previousValue: TEntity;
}
| {
type: EntityMutationType.DELETE;
};
abstract class BaseMutator<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
@@ -110,26 +92,44 @@ abstract class BaseMutator<
}
}
protected async executeMutationTriggersOrValidatorsAsync(
triggersOrValidators:
| EntityMutationTrigger<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
| EntityMutationValidator<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
| undefined,
protected async executeMutationValidatorsAsync(
validators: EntityMutationValidator<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
queryContext: EntityTransactionalQueryContext,
entity: TEntity,
mutationInfo: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
mutationInfo: EntityValidatorMutationInfo<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>
): Promise<void> {
if (!triggersOrValidators) {
return;
}
await Promise.all(
triggersOrValidators.map((triggerOrValidator) =>
triggerOrValidator.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
validators.map((validator) =>
validator.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
)
);
}
protected async executeNonTransactionalMutationTriggersOrValidatorsAsync(
protected async executeMutationTriggersAsync(
triggers:
| EntityMutationTrigger<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
| undefined,
queryContext: EntityTransactionalQueryContext,
entity: TEntity,
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
): Promise<void> {
if (!triggers) {
return;
}
await Promise.all(
triggers.map((trigger) =>
trigger.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
)
);
}
protected async executeNonTransactionalMutationTriggersAsync(
triggers:
| EntityNonTransactionalMutationTrigger<
TFields,
@@ -140,7 +140,7 @@ abstract class BaseMutator<
>[]
| undefined,
entity: TEntity,
mutationInfo: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
): Promise<void> {
if (!triggers) {
return;
@@ -228,19 +228,19 @@ export class CreateMutator<
return authorizeCreateResult;
}
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationValidatorsAsync(
this.mutationValidators,
queryContext,
temporaryEntityForPrivacyCheck,
{ type: EntityMutationType.CREATE }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeAll,
queryContext,
temporaryEntityForPrivacyCheck,
{ type: EntityMutationType.CREATE }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeCreate,
queryContext,
temporaryEntityForPrivacyCheck,
@@ -250,7 +250,7 @@ export class CreateMutator<
const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
queryContext.appendPostCommitCallback(
queryContext.appendPostCommitInvalidationCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
);
@@ -259,13 +259,13 @@ export class CreateMutator<
.enforcing()
.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterCreate,
queryContext,
newEntity,
{ type: EntityMutationType.CREATE }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterAll,
queryContext,
newEntity,
@@ -273,7 +273,7 @@ export class CreateMutator<
);
queryContext.appendPostCommitCallback(
this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
this.executeNonTransactionalMutationTriggersAsync.bind(
this,
this.mutationTriggers.afterCommit,
newEntity,
@@ -417,19 +417,19 @@ export class UpdateMutator<
return authorizeUpdateResult;
}
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationValidatorsAsync(
this.mutationValidators,
queryContext,
entityAboutToBeUpdated,
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeAll,
queryContext,
entityAboutToBeUpdated,
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeUpdate,
queryContext,
entityAboutToBeUpdated,
@@ -445,13 +445,13 @@ export class UpdateMutator<
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
queryContext.appendPostCommitCallback(
queryContext.appendPostCommitInvalidationCallback(
entityLoader.invalidateFieldsAsync.bind(
entityLoader,
this.originalEntity.getAllDatabaseFields()
)
);
queryContext.appendPostCommitCallback(
queryContext.appendPostCommitInvalidationCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
);
@@ -460,13 +460,13 @@ export class UpdateMutator<
.enforcing()
.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterUpdate,
queryContext,
updatedEntity,
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterAll,
queryContext,
updatedEntity,
@@ -474,7 +474,7 @@ export class UpdateMutator<
);
queryContext.appendPostCommitCallback(
this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
this.executeNonTransactionalMutationTriggersAsync.bind(
this,
this.mutationTriggers.afterCommit,
updatedEntity,
@@ -565,7 +565,7 @@ export class DeleteMutator<
this.metricsAdapter,
EntityMetricsMutationType.DELETE,
this.entityClass.name
)(this.deleteInTransactionAsync());
)(this.deleteInTransactionAsync(new Set(), false, null));
}
/**
@@ -576,14 +576,16 @@ export class DeleteMutator<
}
private async deleteInTransactionAsync(
processedEntityIdentifiersFromTransitiveDeletions: Set<string> = new Set(),
skipDatabaseDeletion: boolean = false
processedEntityIdentifiersFromTransitiveDeletions: Set<string>,
skipDatabaseDeletion: boolean,
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
): Promise<Result<void>> {
return await this.queryContext.runInTransactionIfNotInTransactionAsync((innerQueryContext) =>
this.deleteInternalAsync(
innerQueryContext,
processedEntityIdentifiersFromTransitiveDeletions,
skipDatabaseDeletion
skipDatabaseDeletion,
cascadingDeleteCause
)
);
}
@@ -591,7 +593,8 @@ export class DeleteMutator<
private async deleteInternalAsync(
queryContext: EntityTransactionalQueryContext,
processedEntityIdentifiersFromTransitiveDeletions: Set<string>,
skipDatabaseDeletion: boolean
skipDatabaseDeletion: boolean,
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
): Promise<Result<void>> {
const authorizeDeleteResult = await asyncResult(
this.privacyPolicy.authorizeDeleteAsync(
@@ -608,20 +611,21 @@ export class DeleteMutator<
await this.processEntityDeletionForInboundEdgesAsync(
this.entity,
queryContext,
processedEntityIdentifiersFromTransitiveDeletions
processedEntityIdentifiersFromTransitiveDeletions,
cascadingDeleteCause
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeAll,
queryContext,
this.entity,
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.beforeDelete,
queryContext,
this.entity,
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
);
if (!skipDatabaseDeletion) {
@@ -633,29 +637,29 @@ export class DeleteMutator<
}
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
queryContext.appendPostCommitCallback(
queryContext.appendPostCommitInvalidationCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterDelete,
queryContext,
this.entity,
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
);
await this.executeMutationTriggersOrValidatorsAsync(
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterAll,
queryContext,
this.entity,
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
);
queryContext.appendPostCommitCallback(
this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
this.executeNonTransactionalMutationTriggersAsync.bind(
this,
this.mutationTriggers.afterCommit,
this.entity,
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
)
);
@@ -679,7 +683,8 @@ export class DeleteMutator<
private async processEntityDeletionForInboundEdgesAsync(
entity: TEntity,
queryContext: EntityTransactionalQueryContext,
processedEntityIdentifiers: Set<string>
processedEntityIdentifiers: Set<string>,
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
): Promise<void> {
// prevent infinite reference cycles by keeping track of entities already processed
if (processedEntityIdentifiers.has(entity.getUniqueIdentifier())) {
@@ -752,7 +757,11 @@ export class DeleteMutator<
.forDelete(inboundReferenceEntity, queryContext)
.deleteInTransactionAsync(
processedEntityIdentifiers,
/* skipDatabaseDeletion */ true // deletion is handled by DB
/* skipDatabaseDeletion */ true, // deletion is handled by DB
{
entity,
cascadingDeleteCause,
}
)
)
);
@@ -776,7 +785,11 @@ export class DeleteMutator<
.forDelete(inboundReferenceEntity, queryContext)
.deleteInTransactionAsync(
processedEntityIdentifiers,
/* skipDatabaseDeletion */ false
/* skipDatabaseDeletion */ false,
{
entity,
cascadingDeleteCause,
}
)
)
);
+54
View File
@@ -1,6 +1,12 @@
import assert from 'assert';
import EntityQueryContextProvider from './EntityQueryContextProvider';
export type PostCommitCallback = (...args: any) => Promise<any>;
export type PreCommitCallback = (
queryContext: EntityTransactionalQueryContext,
...args: any
) => Promise<any>;
/**
* Entity framework representation of transactional and non-transactional database
@@ -43,13 +49,61 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
}
export class EntityTransactionalQueryContext extends EntityQueryContext {
private readonly postCommitInvalidationCallbacks: PostCommitCallback[] = [];
private readonly postCommitCallbacks: PostCommitCallback[] = [];
private readonly preCommitCallbacks: { callback: PreCommitCallback; order: number }[] = [];
/**
* Schedule a pre-commit callback. These will be run within the transaction right before it is
* committed, and will be run in the order specified. Ordering of callbacks scheduled with the
* same value for the order parameter is undefined within that ordering group.
* @param callback - callback to schedule
* @param order - order in which this should be run relative to other scheduled pre-commit callbacks,
* with higher numbers running later than lower numbers.
*/
public appendPreCommitCallback(callback: PreCommitCallback, order: number): void {
assert(
order >= Number.MIN_SAFE_INTEGER && order <= Number.MAX_SAFE_INTEGER,
`Invalid order specified: ${order}`
);
this.preCommitCallbacks.push({ callback, order });
}
/**
* Schedule a post-commit cache invalidation callback. These are run before normal
* post-commit callbacks in order to have cache consistency in normal post-commit callbacks.
* @param callback - callback to schedule
*/
public appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
this.postCommitInvalidationCallbacks.push(callback);
}
/**
* Schedule a post-commit callback. These will be run after the transaction has
* been committed.
* @param callback - callback to schedule
*/
public appendPostCommitCallback(callback: PostCommitCallback): void {
this.postCommitCallbacks.push(callback);
}
public async runPreCommitCallbacksAsync(): Promise<void> {
const callbacks = [...this.preCommitCallbacks]
.sort((a, b) => a.order - b.order)
.map((c) => c.callback);
this.preCommitCallbacks.length = 0;
for (const callback of callbacks) {
await callback(this);
}
}
public async runPostCommitCallbacksAsync(): Promise<void> {
const invalidationCallbacks = [...this.postCommitInvalidationCallbacks];
this.postCommitInvalidationCallbacks.length = 0;
await Promise.all(invalidationCallbacks.map((callback) => callback()));
const callbacks = [...this.postCommitCallbacks];
this.postCommitCallbacks.length = 0;
await Promise.all(callbacks.map((callback) => callback()));
@@ -38,6 +38,7 @@ export default abstract class EntityQueryContextProvider {
>()(async (queryInterface) => {
const queryContext = new EntityTransactionalQueryContext(queryInterface);
const result = await transactionScope(queryContext);
await queryContext.runPreCommitCallbacksAsync();
return [result, queryContext];
});
await queryContext.runPostCommitCallbacksAsync();
@@ -3,7 +3,10 @@ import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { EntityEdgeDeletionBehavior } from '../EntityFieldDefinition';
import { UUIDField } from '../EntityFields';
import { EntityTriggerMutationInfo, EntityMutationType } from '../EntityMutationInfo';
import { EntityMutationTrigger } from '../EntityMutationTriggerConfiguration';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import { EntityTransactionalQueryContext } from '../EntityQueryContext';
import { CacheStatus } from '../internal/ReadThroughEntityCache';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import TestViewerContext from '../testfixtures/TestViewerContext';
@@ -47,6 +50,125 @@ class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => {
const triggerExecutionCounts = {
parent: 0,
child: 0,
grandchild: 0,
};
class ParentCheckInfoTrigger extends EntityMutationTrigger<
ParentFields,
string,
TestViewerContext,
ParentEntity
> {
async executeAsync(
_viewerContext: TestViewerContext,
_queryContext: EntityTransactionalQueryContext,
_entity: ParentEntity,
mutationInfo: EntityTriggerMutationInfo<ParentFields, string, TestViewerContext, ParentEntity>
): Promise<void> {
if (mutationInfo.type !== EntityMutationType.DELETE) {
return;
}
if (mutationInfo.cascadingDeleteCause !== null) {
throw new Error('Parent entity should not have casade delete cause');
}
triggerExecutionCounts.parent++;
}
}
class ChildCheckInfoTrigger extends EntityMutationTrigger<
ChildFields,
string,
TestViewerContext,
ChildEntity
> {
async executeAsync(
_viewerContext: TestViewerContext,
_queryContext: EntityTransactionalQueryContext,
_entity: ChildEntity,
mutationInfo: EntityTriggerMutationInfo<ChildFields, string, TestViewerContext, ChildEntity>
): Promise<void> {
if (mutationInfo.type !== EntityMutationType.DELETE) {
return;
}
if (mutationInfo.cascadingDeleteCause === null) {
throw new Error('Child entity should have casade delete cause');
}
const cascadingDeleteCauseEntity = mutationInfo.cascadingDeleteCause.entity;
if (!(cascadingDeleteCauseEntity instanceof ParentEntity)) {
throw new Error('Child entity should have casade delete cause entity of type ParentEntity');
}
const secondLevelCascadingDeleteCause =
mutationInfo.cascadingDeleteCause.cascadingDeleteCause;
if (secondLevelCascadingDeleteCause) {
throw new Error('Child entity should not have two-level casade delete cause');
}
triggerExecutionCounts.child++;
}
}
class GrandChildCheckInfoTrigger extends EntityMutationTrigger<
GrandChildFields,
string,
TestViewerContext,
GrandChildEntity
> {
async executeAsync(
_viewerContext: TestViewerContext,
_queryContext: EntityTransactionalQueryContext,
_entity: GrandChildEntity,
mutationInfo: EntityTriggerMutationInfo<
GrandChildFields,
string,
TestViewerContext,
GrandChildEntity
>
): Promise<void> {
if (mutationInfo.type !== EntityMutationType.DELETE) {
return;
}
if (mutationInfo.cascadingDeleteCause === null) {
throw new Error('GrandChild entity should have cascade delete cause');
}
const cascadingDeleteCauseEntity = mutationInfo.cascadingDeleteCause.entity;
if (!(cascadingDeleteCauseEntity instanceof ChildEntity)) {
throw new Error(
'GrandChild entity should have cascade delete cause entity of type ChildEntity'
);
}
const secondLevelCascadingDeleteCause =
mutationInfo.cascadingDeleteCause.cascadingDeleteCause;
if (!secondLevelCascadingDeleteCause) {
throw new Error('GrandChild entity should have two-level casade delete cause');
}
const secondLevelCascadingDeleteCauseEntity = secondLevelCascadingDeleteCause.entity;
if (!(secondLevelCascadingDeleteCauseEntity instanceof ParentEntity)) {
throw new Error(
'GrandChild entity should have second level casade delete cause entity of type ParentEntity'
);
}
const thirdLevelCascadingDeleteCause = secondLevelCascadingDeleteCause.cascadingDeleteCause;
if (thirdLevelCascadingDeleteCause) {
throw new Error('GrandChild entity should not have three-level casade delete cause');
}
triggerExecutionCounts.grandchild++;
}
}
class ParentEntity extends Entity<ParentFields, string, TestViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
ParentFields,
@@ -144,33 +266,45 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
entityClass: ParentEntity,
entityConfiguration: parentEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
mutationTriggers: () => ({
beforeDelete: [new ParentCheckInfoTrigger()],
afterDelete: [new ParentCheckInfoTrigger()],
}),
});
const childEntityCompanion = new EntityCompanionDefinition({
entityClass: ChildEntity,
entityConfiguration: childEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
mutationTriggers: () => ({
beforeDelete: [new ChildCheckInfoTrigger()],
afterDelete: [new ChildCheckInfoTrigger()],
}),
});
const grandChildEntityCompanion = new EntityCompanionDefinition({
entityClass: GrandChildEntity,
entityConfiguration: grandChildEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
mutationTriggers: () => ({
beforeDelete: [new GrandChildCheckInfoTrigger()],
afterDelete: [new GrandChildCheckInfoTrigger()],
}),
});
return {
ParentEntity,
ChildEntity,
GrandChildEntity,
triggerExecutionCounts,
};
};
describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
it('deletes', async () => {
const { ParentEntity, ChildEntity, GrandChildEntity } = makeEntityClasses(
EntityEdgeDeletionBehavior.CASCADE_DELETE
);
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE);
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
@@ -203,14 +337,20 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
expect(triggerExecutionCounts).toMatchObject({
parent: 2,
child: 2,
grandchild: 2,
});
});
});
describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
it('sets null', async () => {
const { ParentEntity, ChildEntity, GrandChildEntity } = makeEntityClasses(
EntityEdgeDeletionBehavior.SET_NULL
);
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
makeEntityClasses(EntityEdgeDeletionBehavior.SET_NULL);
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
@@ -248,14 +388,21 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
.enforcing()
.loadByIDAsync(grandchild.getID());
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
// two calls for only parent trigger, one beforeDelete, one afterDelete
// when using set null the descendants aren't deleted
expect(triggerExecutionCounts).toMatchObject({
parent: 2,
child: 0,
grandchild: 0,
});
});
});
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
it('invalidates the cache', async () => {
const { ParentEntity, ChildEntity, GrandChildEntity } = makeEntityClasses(
EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE
);
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE);
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
@@ -319,6 +466,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
expect(triggerExecutionCounts).toMatchObject({
parent: 2,
child: 2,
grandchild: 2,
});
});
});
});
@@ -0,0 +1,105 @@
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import { EntityMutationType, EntityTriggerMutationInfo } from '../EntityMutationInfo';
import { EntityNonTransactionalMutationTrigger } from '../EntityMutationTriggerConfiguration';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
type BlahFields = {
id: string;
};
class BlahEntityPrivacyPolicy extends EntityPrivacyPolicy<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
}
class BlahEntity extends Entity<BlahFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
BlahFields,
string,
ViewerContext,
BlahEntity,
BlahEntityPrivacyPolicy
> {
return blahCompanion;
}
}
const blahCompanion = new EntityCompanionDefinition({
entityClass: BlahEntity,
entityConfiguration: new EntityConfiguration<BlahFields>({
idField: 'id',
tableName: 'blah_table',
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
}),
privacyPolicyClass: BlahEntityPrivacyPolicy,
mutationTriggers: () => ({
afterCommit: [new TestNonTransactionalMutationTrigger()],
}),
});
class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutationTrigger<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
async executeAsync(
viewerContext: ViewerContext,
entity: BlahEntity,
mutationInfo: EntityTriggerMutationInfo<BlahFields, string, ViewerContext, BlahEntity>
): Promise<void> {
if (mutationInfo.type === EntityMutationType.DELETE) {
const entityLoaded = await BlahEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(entity.getID());
if (entityLoaded) {
throw new Error(
'should not have been able to re-load the entity after delete. this means the cache has not been cleared'
);
}
}
}
}
describe('EntityMutator', () => {
test('cache consistency with post-commit callbacks', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
// 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);
});
});
@@ -14,12 +14,16 @@ import { v4 as uuidv4 } from 'uuid';
import EntityDatabaseAdapter from '../EntityDatabaseAdapter';
import EntityLoaderFactory from '../EntityLoaderFactory';
import {
EntityMutationType,
EntityTriggerMutationInfo,
EntityValidatorMutationInfo,
} from '../EntityMutationInfo';
import EntityMutationTriggerConfiguration, {
EntityMutationTrigger,
EntityNonTransactionalMutationTrigger,
} from '../EntityMutationTriggerConfiguration';
import EntityMutationValidator from '../EntityMutationValidator';
import { EntityMutationInfo, EntityMutationType } from '../EntityMutator';
import EntityMutatorFactory from '../EntityMutatorFactory';
import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext';
import ViewerContext from '../ViewerContext';
@@ -53,7 +57,7 @@ class TestMutationTrigger extends EntityMutationTrigger<
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
_entity: TestEntity,
_mutationInfo: EntityMutationInfo<
_mutationInfo: EntityTriggerMutationInfo<
TestFields,
string,
ViewerContext,
@@ -95,7 +99,13 @@ const verifyValidatorCounts = (
keyof TestFields
>[],
expectedCalls: number,
mutationInfo: EntityMutationInfo<TestFields, string, ViewerContext, TestEntity, keyof TestFields>
mutationInfo: EntityValidatorMutationInfo<
TestFields,
string,
ViewerContext,
TestEntity,
keyof TestFields
>
): void => {
for (const validator of mutationValidatorSpies) {
verify(
@@ -164,7 +174,13 @@ const verifyTriggerCounts = (
>,
boolean
>,
mutationInfo: EntityMutationInfo<TestFields, string, ViewerContext, TestEntity, keyof TestFields>
mutationInfo: EntityTriggerMutationInfo<
TestFields,
string,
ViewerContext,
TestEntity,
keyof TestFields
>
): void => {
Object.keys(executed).forEach((s) => {
if ((executed as any)[s]) {
@@ -767,7 +783,7 @@ describe(EntityMutatorFactory, () => {
beforeDelete: true,
afterDelete: true,
},
{ type: EntityMutationType.DELETE }
{ type: EntityMutationType.DELETE, cascadingDeleteCause: null }
);
});
@@ -796,7 +812,9 @@ describe(EntityMutatorFactory, () => {
await entityMutatorFactory.forDelete(existingEntity, queryContext).enforceDeleteAsync();
verifyValidatorCounts(viewerContext, validatorSpies, 0, { type: EntityMutationType.DELETE });
verifyValidatorCounts(viewerContext, validatorSpies, 0, {
type: EntityMutationType.DELETE as any,
});
});
});
@@ -0,0 +1,82 @@
import invariant from 'invariant';
import { EntityQueryContext } from '../EntityQueryContext';
import ViewerContext from '../ViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
describe(EntityQueryContext, () => {
describe('callbacks', () => {
it('calls all callbacks, and calls invalidation first', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const preCommitFirstCallback = jest.fn(async (): Promise<void> => {});
const preCommitSecondCallback = jest.fn(async (): Promise<void> => {});
const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {
invariant(
preCommitFirstCallback.mock.calls.length === 1,
'preCommit should be called before postCommitInvalidation'
);
invariant(
preCommitSecondCallback.mock.calls.length === 1,
'preCommit should be called before postCommitInvalidation'
);
});
const postCommitCallback = jest.fn(async (): Promise<void> => {
invariant(
preCommitFirstCallback.mock.calls.length === 1,
'preCommit should be called before postCommit'
);
invariant(
preCommitSecondCallback.mock.calls.length === 1,
'preCommit should be called before postCommit'
);
invariant(
postCommitInvalidationCallback.mock.calls.length === 1,
'postCommitInvalidation should be called before postCommit'
);
});
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
'postgres',
async (queryContext) => {
queryContext.appendPostCommitCallback(postCommitCallback);
queryContext.appendPostCommitInvalidationCallback(postCommitInvalidationCallback);
queryContext.appendPreCommitCallback(preCommitSecondCallback, 2);
queryContext.appendPreCommitCallback(preCommitFirstCallback, 1);
}
);
expect(preCommitFirstCallback).toHaveBeenCalledTimes(1);
expect(preCommitSecondCallback).toHaveBeenCalledTimes(1);
expect(postCommitCallback).toHaveBeenCalledTimes(1);
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(1);
});
it('prevents transaction from finishing when precommit throws (post commit callbacks are not called)', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const preCommitCallback = jest.fn(async (): Promise<void> => {
throw new Error('wat');
});
const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {});
const postCommitCallback = jest.fn(async (): Promise<void> => {});
await expect(
viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
'postgres',
async (queryContext) => {
queryContext.appendPostCommitCallback(postCommitCallback);
queryContext.appendPostCommitInvalidationCallback(postCommitInvalidationCallback);
queryContext.appendPreCommitCallback(preCommitCallback, 0);
}
)
).rejects.toThrowError('wat');
expect(preCommitCallback).toHaveBeenCalledTimes(1);
expect(postCommitCallback).toHaveBeenCalledTimes(0);
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(0);
});
});
});
+1
View File
@@ -33,6 +33,7 @@ export { default as EntitySecondaryCacheLoader } from './EntitySecondaryCacheLoa
export * from './EntitySecondaryCacheLoader';
export * from './EntityMutator';
export { default as EntityMutationValidator } from './EntityMutationValidator';
export * from './EntityMutationInfo';
export * from './EntityMutationTriggerConfiguration';
export { default as EntityMutationTriggerConfiguration } from './EntityMutationTriggerConfiguration';
export { default as EntityMutatorFactory } from './EntityMutatorFactory';
@@ -3,11 +3,6 @@ import invariant from 'invariant';
import EntityCacheAdapter from '../EntityCacheAdapter';
import EntityConfiguration from '../EntityConfiguration';
import { filterMap } from '../utils/collections/maps';
import {
FieldTransformerMap,
transformCacheObjectToFields,
transformFieldsToCacheObject,
} from './EntityFieldTransformationUtils';
export enum CacheStatus {
HIT,
@@ -32,14 +27,10 @@ export type CacheLoadResult =
* {@link EntityCacheAdapter} within the {@link EntityDataManager}.
*/
export default class ReadThroughEntityCache<TFields> {
private readonly fieldTransformerMap: FieldTransformerMap;
constructor(
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityCacheAdapter: EntityCacheAdapter<TFields>
) {
this.fieldTransformerMap = entityCacheAdapter.getFieldTransformerMap();
}
) {}
private isFieldCacheable<N extends keyof TFields>(fieldName: N): boolean {
return this.entityConfiguration.cacheableKeys.has(fieldName);
@@ -91,13 +82,7 @@ export default class ReadThroughEntityCache<TFields> {
const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map();
cacheLoadResults.forEach((cacheLoadResult, fieldValue) => {
if (cacheLoadResult.status === CacheStatus.HIT) {
results.set(fieldValue, [
transformCacheObjectToFields(
this.entityConfiguration,
this.fieldTransformerMap,
cacheLoadResult.item
),
]);
results.set(fieldValue, [cacheLoadResult.item as Readonly<TFields>]);
}
});
@@ -123,14 +108,7 @@ export default class ReadThroughEntityCache<TFields> {
}
const uniqueObject = objects[0];
if (uniqueObject) {
objectsToCache.set(
fieldValue,
transformFieldsToCacheObject(
this.entityConfiguration,
this.fieldTransformerMap,
uniqueObject
)
);
objectsToCache.set(fieldValue, uniqueObject);
results.set(fieldValue, [uniqueObject]);
}
}
@@ -40,7 +40,6 @@ describe(ReadThroughEntityCache, () => {
describe('readManyThroughAsync', () => {
it('fetches from DB upon cache miss and caches the result', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']);
@@ -77,7 +76,6 @@ describe(ReadThroughEntityCache, () => {
it('does not fetch from the DB or cache results when all cache fetches are hits', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']);
@@ -114,7 +112,6 @@ describe(ReadThroughEntityCache, () => {
it('negatively caches db misses', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
@@ -135,7 +132,6 @@ describe(ReadThroughEntityCache, () => {
it('does not return or fetch negatively cached results from DB', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher([]);
@@ -153,7 +149,6 @@ describe(ReadThroughEntityCache, () => {
it('does a mix and match of hit, miss, and negative', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who', 'why']);
@@ -189,7 +184,6 @@ describe(ReadThroughEntityCache, () => {
it('does not call into cache for field that is not cacheable', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(new Map());
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(false), cacheAdapter);
const fetcher = createIdFetcher(['wat']);
@@ -197,44 +191,6 @@ describe(ReadThroughEntityCache, () => {
verify(cacheAdapterMock.loadManyAsync('id', anything())).never();
expect(result).toEqual(new Map([['wat', [{ id: 'wat' }]]]));
});
it('transforms fields for cache storage', async () => {
const cacheAdapterMock = mock<EntityCacheAdapter<BlahFields>>();
when(cacheAdapterMock.getFieldTransformerMap()).thenReturn(
new Map([
[
UUIDField.name,
{
read: (val) => val.split('-')[0],
write: (val) => `${val}-in-cache`,
},
],
])
);
const cacheAdapter = instance(cacheAdapterMock);
const entityCache = new ReadThroughEntityCache(makeEntityConfiguration(true), cacheAdapter);
const fetcher = createIdFetcher(['wat', 'who']);
when(cacheAdapterMock.loadManyAsync('id', deepEqual(['wat', 'who']))).thenResolve(
new Map([
['wat', { status: CacheStatus.MISS }],
['who', { status: CacheStatus.HIT, item: { id: 'who-in-cache' } }],
])
);
const result = await entityCache.readManyThroughAsync('id', ['wat', 'who'], fetcher);
verify(cacheAdapterMock.loadManyAsync('id', deepEqual(['wat', 'who']))).once();
verify(
cacheAdapterMock.cacheManyAsync('id', deepEqual(new Map([['wat', { id: 'wat-in-cache' }]])))
).once();
expect(result).toEqual(
new Map([
['wat', [{ id: 'wat' }]],
['who', [{ id: 'who' }]],
])
);
});
});
describe('invalidateManyAsync', () => {
+48 -46
View File
@@ -508,27 +508,27 @@
minimist "^1.2.0"
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
version "0.18.0"
version "0.21.0"
dependencies:
ioredis "^4.27.3"
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
version "0.18.0"
version "0.21.0"
dependencies:
knex "^0.95.6"
knex "^1.0.2"
"@expo/entity-ip-address-field@file:packages/entity-ip-address-field":
version "0.18.0"
version "0.21.0"
dependencies:
ip-address "^8.1.0"
"@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis":
version "0.18.0"
version "0.21.0"
dependencies:
ioredis "^4.17.3"
"@expo/entity@file:packages/entity":
version "0.18.0"
version "0.21.0"
dependencies:
"@expo/results" "^1.0.0"
dataloader "^2.0.0"
@@ -3270,10 +3270,10 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colorette@2.0.16:
version "2.0.16"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
columnify@^1.5.4:
version "1.5.4"
@@ -3295,10 +3295,10 @@ commander@^2.20.3, commander@~2.20.3:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
compare-func@^2.0.0:
version "2.0.0"
@@ -3582,13 +3582,20 @@ dateformat@~1.0.4-1.2.3:
get-stdin "^4.0.1"
meow "^3.3.0"
debug@4, [email protected], debug@^4.3.1:
debug@4, debug@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
[email protected]:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -4694,10 +4701,10 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
getopts@2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b"
integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==
getopts@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==
getpass@^0.1.1:
version "0.1.7"
@@ -6228,23 +6235,23 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
knex@^0.95.6:
version "0.95.6"
resolved "https://registry.yarnpkg.com/knex/-/knex-0.95.6.tgz#5fc60ffc2935567bf122925526b1b06b8dbca785"
integrity sha512-noRcmkJl1MdicUbezrcr8OtVLcqQ/cfLIwgAx5EaxNxQOIJff88rBeyLywUScGhQNd/b78DIKKXZzLMrm6h/cw==
knex@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/knex/-/knex-1.0.2.tgz#1b79273f39f587a631c1a5515482c203d5971781"
integrity sha512-RuDKTylj6X/3nYomnsFV8sOdxTcehLHczOd3yrUdULE4pQR8jVlZxYt3vvIU04otJF0Cw9DCtRt05S4PN4kDpw==
dependencies:
colorette "1.2.1"
commander "^7.1.0"
debug "4.3.1"
colorette "2.0.16"
commander "^8.3.0"
debug "4.3.3"
escalade "^3.1.1"
esm "^3.2.25"
getopts "2.2.5"
getopts "2.3.0"
interpret "^2.2.0"
lodash "^4.17.21"
pg-connection-string "2.4.0"
rechoir "^0.7.0"
pg-connection-string "2.5.0"
rechoir "^0.8.0"
resolve-from "^5.0.0"
tarn "^3.0.1"
tarn "^3.0.2"
tildify "2.0.0"
koa-body@^4.2.0:
@@ -7669,12 +7676,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pg-connection-string@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10"
integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==
pg-connection-string@^2.5.0:
pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
@@ -8139,12 +8141,12 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"
rechoir@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca"
integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==
rechoir@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
dependencies:
resolve "^1.9.0"
resolve "^1.20.0"
redent@^1.0.0:
version "1.0.0"
@@ -8316,7 +8318,7 @@ resolve@^1.18.1, resolve@~1.19.0:
is-core-module "^2.1.0"
path-parse "^1.0.6"
resolve@^1.20.0, resolve@^1.9.0:
resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -9108,10 +9110,10 @@ tar@^6.0.2, tar@^6.1.0:
mkdirp "^1.0.3"
yallist "^4.0.0"
tarn@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec"
integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==
tarn@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693"
integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==
temp-dir@^1.0.0:
version "1.0.0"