Compare commits

..
31 changed files with 969 additions and 316 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
"eslint-plugin-tsdoc"
],
"rules": {
"no-restricted-imports": ["error",{ "paths": [".", ".."] }],
"no-restricted-imports": ["error",{ "paths": [".", "..", "../.."] }],
"tsdoc/syntax": "warn"
},
"overrides": [
@@ -12,7 +12,7 @@
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --config ../../resources/jest.config.js --rootDir",
"test": "jest --rootDir --config ../../resources/jest.config.js",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
@@ -4,8 +4,8 @@ import {
EntityCompanionProvider,
CacheAdapterFlavor,
DatabaseAdapterFlavor,
StubDatabaseAdapter,
StubQueryContextProvider,
StubDatabaseAdapterProvider,
} from '@expo/entity';
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
@@ -19,7 +19,7 @@ export const createRedisIntegrationTestEntityCompanionProvider = (
metricsAdapter,
{
[DatabaseAdapterFlavor.POSTGRES]: {
adapter: StubDatabaseAdapter,
adapterProvider: new StubDatabaseAdapterProvider(),
queryContextProvider: StubQueryContextProvider,
},
},
@@ -12,7 +12,7 @@
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --config ../../resources/jest.config.js --rootDir --passWithNoTests",
"test": "jest --rootDir --config ../../resources/jest.config.js --passWithNoTests",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
@@ -0,0 +1,16 @@
import {
IEntityDatabaseAdapterProvider,
EntityConfiguration,
EntityDatabaseAdapter,
} from '@expo/entity';
import PostgresEntityDatabaseAdapter from './PostgresEntityDatabaseAdapter';
export default class PostgresEntityDatabaseAdapterProvider
implements IEntityDatabaseAdapterProvider {
getDatabaseAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityDatabaseAdapter<TFields> {
return new PostgresEntityDatabaseAdapter(entityConfiguration);
}
}
@@ -8,7 +8,7 @@ import {
} from '@expo/entity';
import Knex from 'knex';
import PostgresEntityDatabaseAdapter from '../PostgresEntityDatabaseAdapter';
import PostgresEntityDatabaseAdapterProvider from '../PostgresEntityDatabaseAdapterProvider';
import PostgresEntityQueryContextProvider from '../PostgresEntityQueryContextProvider';
export const createKnexIntegrationTestEntityCompanionProvider = (
@@ -19,7 +19,7 @@ export const createKnexIntegrationTestEntityCompanionProvider = (
metricsAdapter,
{
[DatabaseAdapterFlavor.POSTGRES]: {
adapter: PostgresEntityDatabaseAdapter,
adapterProvider: new PostgresEntityDatabaseAdapterProvider(),
queryContextProvider: new PostgresEntityQueryContextProvider(knex),
},
},
+2 -2
View File
@@ -7,7 +7,7 @@
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --config ../../resources/jest.config.js --rootDir",
"test": "jest --rootDir --config ../../resources/jest.config.js",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete",
@@ -23,7 +23,7 @@
"license": "MIT",
"dependencies": {
"@expo/entity": "^0.4.0",
"apollo-server-koa": "^2.13.0",
"apollo-server-koa": "^2.14.2",
"graphql": "^15.0.0",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
@@ -7,17 +7,26 @@ import {
TableQuerySelectionModifiers,
getDatabaseFieldForEntityField,
EntityConfiguration,
IEntityDatabaseAdapterProvider,
} from '@expo/entity';
import invariant from 'invariant';
import { v4 as uuidv4 } from 'uuid';
const dbObjects: Readonly<{ [key: string]: any }>[] = [];
export class InMemoryDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
getDatabaseAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityDatabaseAdapter<TFields> {
return new InMemoryDatabaseAdapter(entityConfiguration);
}
}
/**
* In-memory database adapter for entity for the purposes of this example. Normally `@expo/entity-database-adapter-knex`
* or another production adapter would be used. Very similar to StubDatabaseAdapter but shared in a way more akin to a normal database.
*/
export default class InMemoryDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
class InMemoryDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
protected getFieldTransformerMap(): FieldTransformerMap {
return new Map();
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
DatabaseAdapterFlavor,
} from '@expo/entity';
import InMemoryDatabaseAdapter from './adapters/InMemoryDatabaseAdapter';
import { InMemoryDatabaseAdapterProvider } from './adapters/InMemoryDatabaseAdapter';
import InMemoryQueryContextProvider from './adapters/InMemoryQueryContextProvider';
/**
@@ -25,7 +25,7 @@ export const createEntityCompanionProvider = (
// An in-memory DB is used for demonstration purposes, but generally this would be
// instantiated with PostgresEntityDatabaseAdapter and PostgresEntityQueryContextProvider
[DatabaseAdapterFlavor.POSTGRES]: {
adapter: InMemoryDatabaseAdapter,
adapterProvider: new InMemoryDatabaseAdapterProvider(),
queryContextProvider: new InMemoryQueryContextProvider(),
},
},
+1 -1
View File
@@ -12,7 +12,7 @@
"tsc": "tsc",
"clean": "rm -rf build coverage coverage-integration",
"lint": "eslint src --ext '.ts'",
"test": "jest --config ../../resources/jest.config.js --rootDir",
"test": "jest --rootDir --config ../../resources/jest.config.js",
"integration": "../../resources/run-with-docker yarn integration-no-setup",
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir --runInBand --passWithNoTests",
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
+3 -6
View File
@@ -6,6 +6,7 @@ import EntityLoaderFactory from './EntityLoaderFactory';
import EntityMutatorFactory from './EntityMutatorFactory';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider';
import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider';
import IEntityQueryContextProvider from './IEntityQueryContextProvider';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
@@ -13,10 +14,6 @@ import EntityDataManager from './internal/EntityDataManager';
import ReadThroughEntityCache from './internal/ReadThroughEntityCache';
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
export interface IDatabaseAdapterClass<TFields> {
new (entityConfiguration: EntityConfiguration<TFields>): EntityDatabaseAdapter<TFields>;
}
export interface IPrivacyPolicyClass<TPrivacyPolicy> {
new (): TPrivacyPolicy;
}
@@ -53,14 +50,14 @@ export default class EntityCompanion<
constructor(
entityClass: IEntityClass<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy>,
entityConfiguration: EntityConfiguration<TFields>,
DatabaseAdapterClass: IDatabaseAdapterClass<TFields>,
databaseAdapterProvider: IEntityDatabaseAdapterProvider,
cacheAdapterProvider: IEntityCacheAdapterProvider,
PrivacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>,
private readonly queryContextProvider: IEntityQueryContextProvider,
metricsAdapter: IEntityMetricsAdapter
) {
this.databaseAdapter = new DatabaseAdapterClass(entityConfiguration);
this.databaseAdapter = databaseAdapterProvider.getDatabaseAdapter(entityConfiguration);
this.cacheAdapter = cacheAdapterProvider.getCacheAdapter(entityConfiguration);
const dataManager = new EntityDataManager(
this.databaseAdapter,
@@ -1,8 +1,9 @@
import { IEntityClass } from './Entity';
import EntityCompanion, { IDatabaseAdapterClass, IPrivacyPolicyClass } from './EntityCompanion';
import EntityCompanion, { IPrivacyPolicyClass } from './EntityCompanion';
import EntityConfiguration from './EntityConfiguration';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider';
import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider';
import IEntityQueryContextProvider from './IEntityQueryContextProvider';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
@@ -37,8 +38,8 @@ export enum CacheAdapterFlavor {
* will use the specified adapter for database accesses and the specified query context provider
* for providing query contexts.
*/
export interface DatabaseAdapterFlavorDefinition<TFields> {
adapter: IDatabaseAdapterClass<TFields>;
export interface DatabaseAdapterFlavorDefinition {
adapterProvider: IEntityDatabaseAdapterProvider;
queryContextProvider: IEntityQueryContextProvider;
}
@@ -91,10 +92,7 @@ export default class EntityCompanionProvider {
*/
constructor(
private metricsAdapter: IEntityMetricsAdapter,
private databaseAdapterFlavors: Record<
DatabaseAdapterFlavor,
DatabaseAdapterFlavorDefinition<any>
>,
private databaseAdapterFlavors: Record<DatabaseAdapterFlavor, DatabaseAdapterFlavorDefinition>,
private cacheAdapterFlavors: Record<CacheAdapterFlavor, CacheAdapterFlavorDefinition>
) {}
@@ -131,7 +129,7 @@ export default class EntityCompanionProvider {
return new EntityCompanion(
entityCompanionDefinition.entityClass,
entityCompanionDefinition.entityConfiguration,
entityDatabaseAdapterFlavor.adapter,
entityDatabaseAdapterFlavor.adapterProvider,
entityCacheAdapterFlavor.cacheAdapterProvider,
entityCompanionDefinition.privacyPolicyClass,
entityDatabaseAdapterFlavor.queryContextProvider,
+4 -4
View File
@@ -11,7 +11,7 @@ export default class EntityConfiguration<TFields> {
readonly cacheableKeys: ReadonlySet<keyof TFields>;
readonly cacheKeyVersion: number;
readonly schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>;
readonly schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>;
readonly entityToDBFieldsKeyMapping: ReadonlyMap<keyof TFields, string>;
readonly dbToEntityFieldsKeyMapping: ReadonlyMap<string, keyof TFields>;
@@ -23,7 +23,7 @@ export default class EntityConfiguration<TFields> {
}: {
idField: keyof TFields;
tableName: string;
schema: Record<keyof TFields, EntityFieldDefinition>;
schema: Record<keyof TFields, EntityFieldDefinition<any>>;
cacheKeyVersion?: number;
}) {
this.idField = idField;
@@ -43,7 +43,7 @@ export default class EntityConfiguration<TFields> {
}
private static computeCacheableKeys<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlySet<keyof TFields> {
return reduceMap(
schema,
@@ -58,7 +58,7 @@ export default class EntityConfiguration<TFields> {
}
private static computeEntityToDBFieldsKeyMapping<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition>
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlyMap<keyof TFields, string> {
return mapMap(schema, (v) => v.columnName);
}
+33 -11
View File
@@ -1,6 +1,17 @@
export abstract class EntityFieldDefinition {
export interface FieldValidator<T> {
/**
* Validation to apply for this field before an entity is created or updated.
* Only applies to fields changed during a mutation; any existing fields will
* not be re-validated in order to allow for changing of validator logic.
*/
write?: (value: T) => Promise<void>;
}
export abstract class EntityFieldDefinition<T> {
readonly columnName: string;
readonly cache: boolean;
readonly validator: FieldValidator<T>;
/**
*
* @param columnName - Column name in the database.
@@ -8,19 +19,30 @@ export abstract class EntityFieldDefinition {
* used to derive a cache key for the cache entry. If true, this column must be able uniquely
* identify the entity.
*/
constructor({ columnName, cache = false }: { columnName: string; cache?: boolean }) {
constructor({
columnName,
cache = false,
validator = {},
}: {
columnName: string;
cache?: boolean;
validator?: FieldValidator<T>;
}) {
this.columnName = columnName;
this.cache = cache;
this.validator = validator;
}
}
export class StringField extends EntityFieldDefinition {}
export class StringField extends EntityFieldDefinition<string> {}
export class UUIDField extends StringField {}
export class DateField extends EntityFieldDefinition {}
export class BooleanField extends EntityFieldDefinition {}
export class NumberField extends EntityFieldDefinition {}
export class StringArrayField extends EntityFieldDefinition {}
export class JSONObjectField extends EntityFieldDefinition {}
export class EnumField extends EntityFieldDefinition {}
export class JSONArrayField extends EntityFieldDefinition {}
export class MaybeJSONArrayField extends EntityFieldDefinition {}
export class DateField extends EntityFieldDefinition<Date> {}
export class BooleanField extends EntityFieldDefinition<boolean> {}
export class NumberField extends EntityFieldDefinition<number> {}
export class StringArrayField extends EntityFieldDefinition<string[]> {}
export class JSONObjectField<T extends object> extends EntityFieldDefinition<T> {}
export class EnumField<T> extends EntityFieldDefinition<T> {}
export class JSONArrayField<T> extends EntityFieldDefinition<T[]> {}
export class MaybeJSONArrayField<TArray, TNotArray> extends EntityFieldDefinition<
TArray[] | TNotArray
> {}
+33 -3
View File
@@ -1,4 +1,5 @@
import { Result, asyncResult, result, enforceAsyncResult } from '@expo/results';
import invariant from 'invariant';
import _ from 'lodash';
import Entity, { IEntityClass } from './Entity';
@@ -40,6 +41,25 @@ abstract class BaseMutator<
protected readonly databaseAdapter: EntityDatabaseAdapter<TFields>,
protected readonly metricsAdapter: IEntityMetricsAdapter
) {}
protected async validateFields(fields: Partial<TFields>): Promise<Result<Partial<TFields>>> {
const validatorResults = await asyncResult(
Promise.all(
Object.entries(fields).map(async ([fieldName, fieldValue]) => {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName as keyof TFields);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
const writeValidator = fieldDefinition.validator.write;
if (writeValidator) {
await writeValidator(fieldValue);
}
})
)
);
if (!validatorResults.ok) {
return result(validatorResults.reason);
}
return result(fields);
}
}
/**
@@ -84,9 +104,14 @@ export class CreateMutator<
}
private async createInternalAsync(): Promise<Result<TEntity>> {
const validatedFieldsResult = await this.validateFields(this.fieldsForEntity);
if (!validatedFieldsResult.ok) {
return result(validatedFieldsResult.reason);
}
const temporaryEntityForPrivacyCheck = new this.entityClass(this.viewerContext, ({
[this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
...this.fieldsForEntity,
...validatedFieldsResult.value,
} as unknown) as TFields);
const authorizeCreateResult = await asyncResult(
@@ -102,7 +127,7 @@ export class CreateMutator<
const insertResult = await this.databaseAdapter.insertAsync(
this.queryContext,
this.fieldsForEntity
validatedFieldsResult.value
);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
@@ -184,6 +209,11 @@ export class UpdateMutator<
}
private async updateInternalAsync(): Promise<Result<TEntity>> {
const validatedUpdatedFieldsResult = await this.validateFields(this.updatedFields);
if (!validatedUpdatedFieldsResult.ok) {
return result(validatedUpdatedFieldsResult.reason);
}
const entityAboutToBeUpdated = new this.entityClass(this.viewerContext, this.fieldsForEntity);
const authorizeUpdateResult = await asyncResult(
this.privacyPolicy.authorizeUpdateAsync(
@@ -200,7 +230,7 @@ export class UpdateMutator<
this.queryContext,
this.entityConfiguration.idField,
entityAboutToBeUpdated.getID(),
this.updatedFields
validatedUpdatedFieldsResult.value
);
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, this.queryContext);
+3 -2
View File
@@ -50,9 +50,10 @@ export enum EntityAuthorizationAction {
*
* ```
* foreach rule in rules:
* authorized if rule allows
* return authorized if rule allows
* return not authorized if rule denies
* continue to next rule if rule skips
* not authorized if rule denies
* return not authorized if all rules skip
* ```
*/
export default abstract class EntityPrivacyPolicy<
@@ -0,0 +1,15 @@
import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
/**
* A database adapter provider vends database adapters for a particular database adapter type.
* Allows for passing global configuration to databse adapters, making testing easier.
*/
export default interface IEntityDatabaseAdapterProvider {
/**
* Vend a database adapter.
*/
getDatabaseAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityDatabaseAdapter<TFields>;
}
@@ -10,14 +10,14 @@ import TestEntity, {
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
import StubDatabaseAdapterProvider from '../utils/testing/StubDatabaseAdapterProvider';
describe(EntityCompanion, () => {
it('correctly instantiates mutator and loader factories', () => {
const companion = new EntityCompanion(
TestEntity,
testEntityConfiguration,
StubDatabaseAdapter,
new StubDatabaseAdapterProvider(),
new NoCacheStubCacheAdapterProvider(),
TestEntityPrivacyPolicy,
instance(mock<IEntityQueryContextProvider>()),
@@ -1,6 +1,6 @@
import { EntityFieldDefinition } from '../EntityFields';
class TestFieldDefinition extends EntityFieldDefinition {}
class TestFieldDefinition extends EntityFieldDefinition<any> {}
describe(EntityFieldDefinition, () => {
it('returns correct column name and defaults cache to false', () => {
@@ -12,4 +12,21 @@ describe(EntityFieldDefinition, () => {
expect(fieldDefinition2.columnName).toEqual('wat');
expect(fieldDefinition2.cache).toEqual(true);
});
it('defaults validator to no-op', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validator).toEqual({});
const fn = async (): Promise<void> => {
throw new Error();
};
const fieldDefinition2 = new TestFieldDefinition({
columnName: 'wat',
cache: true,
validator: {
write: fn,
},
});
expect(fieldDefinition2.validator.write).toEqual(fn);
});
});
@@ -23,22 +23,33 @@ describe(EntityLoader, () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 5,
stringField: 'huh',
dateField: dateToInsert,
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'huh',
dateField: dateToInsert,
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 5,
stringField: 'huh',
dateField: dateToInsert,
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'huh',
dateField: dateToInsert,
},
],
],
])
)
);
const privacyPolicy = new TestEntityPrivacyPolicy();
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
@@ -87,29 +98,40 @@ describe(EntityLoader, () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
stringField: 'huh',
numberField: 4,
testIndexedField: '4',
dateField: new Date(),
},
{
customIdField: 'world',
stringField: 'huh',
numberField: 4,
testIndexedField: '5',
dateField: new Date(),
},
{
customIdField: 'blah',
stringField: 'huh2',
numberField: 4,
testIndexedField: '6',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
stringField: 'huh',
numberField: 4,
testIndexedField: '4',
dateField: new Date(),
},
{
customIdField: 'world',
stringField: 'huh',
numberField: 4,
testIndexedField: '5',
dateField: new Date(),
},
{
customIdField: 'blah',
stringField: 'huh2',
numberField: 4,
testIndexedField: '6',
dateField: new Date(),
},
],
],
])
)
);
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -152,15 +174,26 @@ describe(EntityLoader, () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
stringField: 'huh',
testIndexedField: '1',
numberField: 3,
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
stringField: 'huh',
testIndexedField: '1',
numberField: 3,
dateField: new Date(),
},
],
],
])
)
);
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -30,6 +30,11 @@ import TestEntity, {
TestEntityPrivacyPolicy,
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import ValidationTestEntity, {
ValidationTestEntityPrivacyPolicy,
ValidationTestFields,
validationTestEntityConfiguration,
} from '../testfixtures/ValidationTestEntity';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider';
@@ -57,7 +62,10 @@ const createEntityMutatorFactory = (
const privacyPolicy = new TestEntityPrivacyPolicy();
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
existingObjects
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([[testEntityConfiguration.tableName, existingObjects]])
)
);
const metricsAdapter = new NoOpEntityMetricsAdapter();
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
@@ -92,7 +100,7 @@ const createEntityMutatorFactory = (
};
describe(EntityMutatorFactory, () => {
it('creates entities and checks privacy', async () => {
it('creates entities checks privacy', async () => {
const viewerContext = mock<ViewerContext>();
const queryContext = mock<EntityQueryContext>();
const { privacyPolicy, entityMutatorFactory } = createEntityMutatorFactory([
@@ -243,6 +251,73 @@ describe(EntityMutatorFactory, () => {
expect(entities2).toHaveLength(2);
});
it('returns error when field not valid', async () => {
const viewerContext = mock<ViewerContext>();
const queryContext = mock<EntityQueryContext>();
const privacyPolicy = new ValidationTestEntityPrivacyPolicy();
const databaseAdapter = new StubDatabaseAdapter<ValidationTestFields>(
validationTestEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
validationTestEntityConfiguration,
new Map([[validationTestEntityConfiguration.tableName, []]])
)
);
const metricsAdapter = new NoOpEntityMetricsAdapter();
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(validationTestEntityConfiguration);
const entityCache = new ReadThroughEntityCache<ValidationTestFields>(
validationTestEntityConfiguration,
cacheAdapter
);
const dataManager = new EntityDataManager(
databaseAdapter,
entityCache,
StubQueryContextProvider,
metricsAdapter
);
const entityLoaderFactory = new EntityLoaderFactory(
validationTestEntityConfiguration,
ValidationTestEntity,
privacyPolicy,
dataManager
);
const entityMutatorFactory = new EntityMutatorFactory(
validationTestEntityConfiguration,
ValidationTestEntity,
privacyPolicy,
entityLoaderFactory,
databaseAdapter,
metricsAdapter
);
const entityCreateInvalidResult = await entityMutatorFactory
.forCreate(viewerContext, queryContext)
.setField('numberOtherThanTen', 10)
.createAsync();
expect(entityCreateInvalidResult.ok).toBe(false);
expect(entityCreateInvalidResult.reason?.message).toEqual('should not be ten');
const entityCreateValidResult = await entityMutatorFactory
.forCreate(viewerContext, queryContext)
.setField('numberOtherThanTen', 11)
.createAsync();
expect(entityCreateValidResult.ok).toBe(true);
const entityUpdateInvalidResult = await entityMutatorFactory
.forUpdate(entityCreateValidResult.enforceValue(), queryContext)
.setField('numberOtherThanTen', 10)
.updateAsync();
expect(entityUpdateInvalidResult.ok).toBe(false);
expect(entityUpdateInvalidResult.reason?.message).toEqual('should not be ten');
const entityUpdateValidResult = await entityMutatorFactory
.forUpdate(entityCreateValidResult.enforceValue(), queryContext)
.setField('numberOtherThanTen', 12)
.updateAsync();
expect(entityUpdateValidResult.ok).toBe(true);
});
it('returns error result when not authorized to create', async () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
+2
View File
@@ -21,6 +21,7 @@ export { default as EntityPrivacyPolicy } from './EntityPrivacyPolicy';
export * from './EntityPrivacyPolicy';
export * from './EntityQueryContext';
export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterProvider';
export { default as IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider';
export { default as IEntityQueryContextProvider } from './IEntityQueryContextProvider';
export { default as ReadonlyEntity } from './ReadonlyEntity';
export { default as ViewerContext } from './ViewerContext';
@@ -45,6 +46,7 @@ export * from './rules/PrivacyPolicyRule';
export * from './utils/testing/PrivacyPolicyRuleTestUtils';
export * from './utils/testing/StubCacheAdapter';
export { default as StubDatabaseAdapter } from './utils/testing/StubDatabaseAdapter';
export { default as StubDatabaseAdapterProvider } from './utils/testing/StubDatabaseAdapterProvider';
export { default as StubQueryContextProvider } from './utils/testing/StubQueryContextProvider';
export * from './utils/testing/createUnitTestEntityCompanionProvider';
export * from './utils/collections/maps';
@@ -24,33 +24,44 @@ import StubQueryContextProvider from '../../utils/testing/StubQueryContextProvid
import EntityDataManager from '../EntityDataManager';
import ReadThroughEntityCache from '../ReadThroughEntityCache';
const objects: TestFields[] = [
{
customIdField: '1',
testIndexedField: 'unique1',
stringField: 'hello',
numberField: 1,
dateField: new Date(),
},
{
customIdField: '2',
testIndexedField: 'unique2',
stringField: 'hello',
numberField: 1,
dateField: new Date(),
},
{
customIdField: '3',
testIndexedField: 'unique3',
stringField: 'world',
numberField: 1,
dateField: new Date(),
},
];
const getObjects = (): Map<string, TestFields[]> =>
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: '1',
testIndexedField: 'unique1',
stringField: 'hello',
numberField: 1,
dateField: new Date(),
},
{
customIdField: '2',
testIndexedField: 'unique2',
stringField: 'hello',
numberField: 1,
dateField: new Date(),
},
{
customIdField: '3',
testIndexedField: 'unique3',
stringField: 'world',
numberField: 1,
dateField: new Date(),
},
],
],
]);
describe(EntityDataManager, () => {
it('loads from db with a no-cache adapter', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new NoCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -92,7 +103,12 @@ describe(EntityDataManager, () => {
});
it('loads from a caching adaptor', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -134,7 +150,12 @@ describe(EntityDataManager, () => {
});
it('loads from a caching adapter with a cache hit', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -171,7 +192,12 @@ describe(EntityDataManager, () => {
});
it('loads from data loader for same query', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -201,7 +227,12 @@ describe(EntityDataManager, () => {
});
it('loads and in-memory caches (dataloader) non-unique, non-cacheable loads', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -239,7 +270,12 @@ describe(EntityDataManager, () => {
});
it('invalidates objects', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -251,7 +287,7 @@ describe(EntityDataManager, () => {
);
const queryContext = StubQueryContextProvider.getRegularEntityQueryContext();
const objectInQuestion = objects[1];
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1];
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -272,7 +308,12 @@ describe(EntityDataManager, () => {
});
it('invalidates all fields for an object', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -284,7 +325,7 @@ describe(EntityDataManager, () => {
);
const queryContext = StubQueryContextProvider.getRegularEntityQueryContext();
const objectInQuestion = objects[1];
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1];
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -305,7 +346,12 @@ describe(EntityDataManager, () => {
});
it('loads only from DB when in transaction', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -337,7 +383,12 @@ describe(EntityDataManager, () => {
});
it('loads by field equality conjunction and does not cache', async () => {
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -403,7 +454,12 @@ describe(EntityDataManager, () => {
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
const metricsAdapter = instance(metricsAdapterMock);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, objects);
const objects = getObjects();
const dataStore = StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
objects
);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, dataStore);
const cacheAdapterProvider = new InMemoryFullCacheStubCacheAdapterProvider();
const cacheAdapter = cacheAdapterProvider.getCacheAdapter(testEntityConfiguration);
const entityCache = new ReadThroughEntityCache(testEntityConfiguration, cacheAdapter);
@@ -0,0 +1,65 @@
import Entity from '../Entity';
import {
DatabaseAdapterFlavor,
CacheAdapterFlavor,
EntityCompanionDefinition,
} from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { DateField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type DateIDTestFields = {
id: Date;
};
export const dateIDTestEntityConfiguration = new EntityConfiguration<DateIDTestFields>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
schema: {
id: new DateField({
columnName: 'custom_id',
}),
},
});
export class DateIDTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
DateIDTestFields,
Date,
ViewerContext,
DateIDTestEntity
> {
protected readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
];
protected readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
];
protected readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
];
protected readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<DateIDTestFields, Date, ViewerContext, DateIDTestEntity>(),
];
}
export default class DateIDTestEntity extends Entity<DateIDTestFields, Date, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
DateIDTestFields,
Date,
ViewerContext,
DateIDTestEntity,
DateIDTestEntityPrivacyPolicy
> {
return dateIDTestEntityCompanion;
}
}
export const dateIDTestEntityCompanion = {
entityClass: DateIDTestEntity,
entityConfiguration: dateIDTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: DateIDTestEntityPrivacyPolicy,
};
@@ -0,0 +1,65 @@
import Entity from '../Entity';
import {
DatabaseAdapterFlavor,
CacheAdapterFlavor,
EntityCompanionDefinition,
} from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { NumberField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type NumberKeyFields = {
id: number;
};
export const numberKeyEntityConfiguration = new EntityConfiguration<NumberKeyFields>({
idField: 'id',
tableName: 'simple_test_entity_should_not_write_to_db',
schema: {
id: new NumberField({
columnName: 'custom_id',
}),
},
});
export class NumberKeyPrivacyPolicy extends EntityPrivacyPolicy<
NumberKeyFields,
number,
ViewerContext,
NumberKeyEntity
> {
protected readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
];
protected readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
];
protected readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
];
protected readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<NumberKeyFields, number, ViewerContext, NumberKeyEntity>(),
];
}
export default class NumberKeyEntity extends Entity<NumberKeyFields, number, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
NumberKeyFields,
number,
ViewerContext,
NumberKeyEntity,
NumberKeyPrivacyPolicy
> {
return numberKeyEntityCompanion;
}
}
export const numberKeyEntityCompanion = {
entityClass: NumberKeyEntity,
entityConfiguration: numberKeyEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: NumberKeyPrivacyPolicy,
};
@@ -0,0 +1,72 @@
import Entity from '../Entity';
import {
DatabaseAdapterFlavor,
CacheAdapterFlavor,
EntityCompanionDefinition,
} from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, NumberField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type ValidationTestFields = {
id: string;
numberOtherThanTen: number;
};
export const validationTestEntityConfiguration = new EntityConfiguration<ValidationTestFields>({
idField: 'id',
tableName: 'validation_test_entity',
schema: {
id: new UUIDField({
columnName: 'custom_id',
}),
numberOtherThanTen: new NumberField({
columnName: 'not_ten',
validator: {
async write(value) {
if (value === 10) {
throw new Error('should not be ten');
}
},
},
}),
},
});
export class ValidationTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
ValidationTestFields,
string,
ViewerContext,
ValidationTestEntity
> {
protected readonly readRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly createRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly updateRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly deleteRules = [new AlwaysAllowPrivacyPolicyRule()];
}
export default class ValidationTestEntity extends Entity<
ValidationTestFields,
string,
ViewerContext
> {
static getCompanionDefinition(): EntityCompanionDefinition<
ValidationTestFields,
string,
ViewerContext,
ValidationTestEntity,
ValidationTestEntityPrivacyPolicy
> {
return validationTestEntityCompanion;
}
}
export const validationTestEntityCompanion = {
entityClass: ValidationTestEntity,
entityConfiguration: validationTestEntityConfiguration,
databaseAdaptorFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdaptorFlavor: CacheAdapterFlavor.REDIS,
privacyPolicyClass: ValidationTestEntityPrivacyPolicy,
};
@@ -8,27 +8,35 @@ import EntityDatabaseAdapter, {
TableQuerySelectionModifiers,
OrderByOrdering,
} from '../../EntityDatabaseAdapter';
import { StringField, NumberField } from '../../EntityFields';
import {
getDatabaseFieldForEntityField,
FieldTransformerMap,
transformFieldsToDatabaseObject,
} from '../../internal/EntityFieldTransformationUtils';
import { computeIfAbsent, mapMap } from '../collections/maps';
export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
private objects: Readonly<{ [key: string]: any }>[];
constructor(
private readonly entityConfiguration2: EntityConfiguration<T>,
prepopulatedObjects: Readonly<T>[] = []
private readonly dataStore: Map<string, Readonly<{ [key: string]: any }>[]>
) {
super(entityConfiguration2);
this.objects = prepopulatedObjects.map((o) =>
transformFieldsToDatabaseObject(entityConfiguration2, this.getFieldTransformerMap(), o)
}
public static convertFieldObjectsToDataStore<T>(
entityConfiguration: EntityConfiguration<T>,
dataStore: Map<string, Readonly<T>[]>
): Map<string, Readonly<{ [key: string]: any }>[]> {
return mapMap(dataStore, (objectsForTable) =>
objectsForTable.map((objectForTable) =>
transformFieldsToDatabaseObject(entityConfiguration, new Map(), objectForTable)
)
);
}
public getAllObjectsForTest(): Readonly<object>[] {
return this.objects;
public getObjectCollectionForTable(tableName: string): { [key: string]: any }[] {
return computeIfAbsent(this.dataStore, tableName, () => []);
}
protected getFieldTransformerMap(): FieldTransformerMap {
@@ -37,13 +45,14 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
protected async fetchManyWhereInternalAsync(
_queryInterface: any,
_tableName: string,
tableName: string,
tableField: string,
tableValues: readonly any[]
): Promise<object[]> {
const objectCollection = this.getObjectCollectionForTable(tableName);
return tableValues.reduce((acc, fieldValue) => {
return acc.concat(
this.objects.filter((obj) => {
objectCollection.filter((obj) => {
return obj[tableField] === fieldValue;
})
);
@@ -83,12 +92,12 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
_queryInterface: any,
_tableName: string,
tableName: string,
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
querySelectionModifiers: TableQuerySelectionModifiers
): Promise<object[]> {
let filteredObjects = this.objects;
let filteredObjects = this.getObjectCollectionForTable(tableName);
for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue);
}
@@ -127,26 +136,42 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter');
}
private generateRandomID(): any {
const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
invariant(idSchemaField, `No schema field found for ${this.entityConfiguration2.idField}`);
if (idSchemaField instanceof StringField) {
return uuidv4();
} else if (idSchemaField instanceof NumberField) {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
} else {
throw new Error(
`Unsupported ID type for StubDatabaseAdapter: ${idSchemaField.constructor.name}`
);
}
}
protected async insertInternalAsync(
_queryInterface: any,
_tableName: string,
tableName: string,
object: object
): Promise<object[]> {
const objectCollection = this.getObjectCollectionForTable(tableName);
const idField = getDatabaseFieldForEntityField(
this.entityConfiguration2,
this.entityConfiguration2.idField
);
const objectToInsert = {
[idField]: uuidv4(),
[idField]: this.generateRandomID(),
...object,
};
this.objects.push(objectToInsert);
objectCollection.push(objectToInsert);
return [objectToInsert];
}
protected async updateInternalAsync(
_queryInterface: any,
_tableName: string,
tableName: string,
tableIdField: string,
id: any,
object: object
@@ -156,28 +181,32 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
throw new Error(`Empty update (${tableIdField} = ${id})`);
}
const objectIndex = this.objects.findIndex((obj) => {
const objectCollection = this.getObjectCollectionForTable(tableName);
const objectIndex = objectCollection.findIndex((obj) => {
return obj[tableIdField] === id;
});
invariant(objectIndex >= 0, 'should exist');
this.objects[objectIndex] = {
...this.objects[objectIndex],
objectCollection[objectIndex] = {
...objectCollection[objectIndex],
...object,
};
return [this.objects[objectIndex]];
return [objectCollection[objectIndex]];
}
protected async deleteInternalAsync(
_queryInterface: any,
_tableName: string,
tableName: string,
tableIdField: string,
id: any
): Promise<number> {
const objectIndex = this.objects.findIndex((obj) => {
const objectCollection = this.getObjectCollectionForTable(tableName);
const objectIndex = objectCollection.findIndex((obj) => {
return obj[tableIdField] === id;
});
invariant(objectIndex >= 0, 'should exist');
this.objects.splice(objectIndex, 1);
objectCollection.splice(objectIndex, 1);
return 1;
}
}
@@ -0,0 +1,14 @@
import EntityConfiguration from '../../EntityConfiguration';
import EntityDatabaseAdapter from '../../EntityDatabaseAdapter';
import IEntityDatabaseAdapterProvider from '../../IEntityDatabaseAdapterProvider';
import StubDatabaseAdapter from './StubDatabaseAdapter';
export default class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
private readonly objectCollection = new Map();
getDatabaseAdapter<TFields>(
entityConfiguration: EntityConfiguration<TFields>
): EntityDatabaseAdapter<TFields> {
return new StubDatabaseAdapter(entityConfiguration, this.objectCollection);
}
}
@@ -2,29 +2,52 @@ import { instance, mock } from 'ts-mockito';
import { OrderByOrdering } from '../../../EntityDatabaseAdapter';
import { EntityNonTransactionalQueryContext } from '../../../EntityQueryContext';
import {
DateIDTestFields,
dateIDTestEntityConfiguration,
} from '../../../testfixtures/DateIDTestEntity';
import {
SimpleTestFields,
simpleTestEntityConfiguration,
} from '../../../testfixtures/SimpleTestEntity';
import { TestFields, testEntityConfiguration } from '../../../testfixtures/TestEntity';
import {
NumberKeyFields,
numberKeyEntityConfiguration,
} from '../../../testfixtures/TestEntityNumberKey';
import StubDatabaseAdapter from '../StubDatabaseAdapter';
describe(StubDatabaseAdapter, () => {
describe('fetchManyWhereAsync', () => {
it('fetches many where', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 5,
stringField: 'huh',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'wat',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 5,
stringField: 'huh',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'wat',
dateField: new Date(),
},
],
],
])
)
);
const results = await databaseAdapter.fetchManyWhereAsync(queryContext, 'stringField', [
'huh',
@@ -36,29 +59,40 @@ describe(StubDatabaseAdapter, () => {
describe('fetchManyByFieldEqualityConjunctionAsync', () => {
it('supports conjuntions and query modifiers', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'b',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'c',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'b',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'c',
dateField: new Date(),
},
],
],
])
)
);
const results = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
@@ -90,29 +124,40 @@ describe(StubDatabaseAdapter, () => {
it('supports multiple order bys', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'b',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'c',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'b',
dateField: new Date(),
},
{
customIdField: 'world',
testIndexedField: 'h2',
numberField: 3,
stringField: 'c',
dateField: new Date(),
},
],
],
])
)
);
const results = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
@@ -144,7 +189,10 @@ describe(StubDatabaseAdapter, () => {
describe('fetchManyByRawWhereClauseAsync', () => {
it('throws because it is unsupported', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, []);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
new Map()
);
await expect(
databaseAdapter.fetchManyByRawWhereClauseAsync(queryContext, '', [], {})
).rejects.toThrow();
@@ -154,7 +202,10 @@ describe(StubDatabaseAdapter, () => {
describe('insertAsync', () => {
it('inserts a record', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, []);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
new Map()
);
const result = await databaseAdapter.insertAsync(queryContext, {
stringField: 'hello',
});
@@ -162,22 +213,35 @@ describe(StubDatabaseAdapter, () => {
stringField: 'hello',
});
expect(databaseAdapter.getAllObjectsForTest()).toHaveLength(1);
expect(
databaseAdapter.getObjectCollectionForTable(testEntityConfiguration.tableName)
).toHaveLength(1);
});
});
describe('updateAsync', () => {
it('updates a record', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
],
],
])
)
);
const result = await databaseAdapter.updateAsync(queryContext, 'customIdField', 'hello', {
stringField: 'b',
});
@@ -191,19 +255,57 @@ describe(StubDatabaseAdapter, () => {
describe('deleteAsync', () => {
it('deletes an object', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(testEntityConfiguration, [
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
]);
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: 'hello',
testIndexedField: 'h1',
numberField: 3,
stringField: 'a',
dateField: new Date(),
},
],
],
])
)
);
await databaseAdapter.deleteAsync(queryContext, 'customIdField', 'hello');
expect(databaseAdapter.getAllObjectsForTest()).toHaveLength(0);
expect(
databaseAdapter.getObjectCollectionForTable(testEntityConfiguration.tableName)
).toHaveLength(0);
});
});
it('supports string and number IDs', async () => {
const queryContext = instance(mock(EntityNonTransactionalQueryContext));
const databaseAdapter1 = new StubDatabaseAdapter<SimpleTestFields>(
simpleTestEntityConfiguration,
new Map()
);
const insertedObject1 = await databaseAdapter1.insertAsync(queryContext, {});
expect(typeof insertedObject1.id).toBe('string');
const databaseAdapter2 = new StubDatabaseAdapter<NumberKeyFields>(
numberKeyEntityConfiguration,
new Map()
);
const insertedObject2 = await databaseAdapter2.insertAsync(queryContext, {});
expect(typeof insertedObject2.id).toBe('number');
const databaseAdapter3 = new StubDatabaseAdapter<DateIDTestFields>(
dateIDTestEntityConfiguration,
new Map()
);
await expect(databaseAdapter3.insertAsync(queryContext, {})).rejects.toThrowError(
'Unsupported ID type for StubDatabaseAdapter: DateField'
);
});
});
@@ -5,7 +5,7 @@ import EntityCompanionProvider, {
import IEntityMetricsAdapter from '../../metrics/IEntityMetricsAdapter';
import NoOpEntityMetricsAdapter from '../../metrics/NoOpEntityMetricsAdapter';
import { InMemoryFullCacheStubCacheAdapterProvider } from './StubCacheAdapter';
import StubDatabaseAdapter from './StubDatabaseAdapter';
import StubDatabaseAdapterProvider from './StubDatabaseAdapterProvider';
import StubQueryContextProvider from './StubQueryContextProvider';
/**
@@ -19,7 +19,7 @@ export const createUnitTestEntityCompanionProvider = (
metricsAdapter,
{
[DatabaseAdapterFlavor.POSTGRES]: {
adapter: StubDatabaseAdapter,
adapterProvider: new StubDatabaseAdapterProvider(),
queryContextProvider: StubQueryContextProvider,
},
},
+108 -73
View File
@@ -311,17 +311,17 @@
which "^1.3.1"
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
version "0.3.0"
version "0.4.0"
dependencies:
ioredis "^4.16.1"
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
version "0.3.0"
version "0.4.0"
dependencies:
knex "^0.20.11"
"@expo/entity@file:packages/entity":
version "0.3.0"
version "0.4.0"
dependencies:
"@expo/results" "^0.3.0"
dataloader "^2.0.0"
@@ -1949,42 +1949,43 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
apollo-cache-control@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.10.0.tgz#ed056d1ce4fb520cab584890a2132a8429f346a1"
integrity sha512-UIcPlrPdRTOKrF7kc5/WD5i6EVkGEEqgOK/fMj92fnnxR1KnQDiN82lqaxu02eZJvWjFJjik0JVJNXKOJXVrpQ==
apollo-cache-control@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.11.0.tgz#7075492d04c5424e7c6769380b503e8f75b39d61"
integrity sha512-dmRnQ9AXGw2SHahVGLzB/p4UW/taFBAJxifxubp8hqY5p9qdlSu4MPRq8zvV2ULMYf50rBtZyC4C+dZLqmHuHQ==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.12.0"
apollo-server-env "^2.4.4"
apollo-server-plugin-base "^0.9.0"
apollo-datasource@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.0.tgz#2a6d82edb2eba21b4ddf21877009ba39ff821945"
integrity sha512-Yja12BgNQhzuFGG/5Nw2MQe0hkuQy2+9er09HxeEyAf2rUDIPnhPrn1MDoZTB8MU7UGfjwITC+1ofzKkkrZobA==
apollo-datasource@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.1.tgz#0b06da999ace50b7f5fe509f2a03f7de97974334"
integrity sha512-h++/jQAY7GA+4TBM+7ezvctFmmGNLrAPf51KsagZj+NkT9qvxp585rdsuatynVbSl59toPK2EuVmc6ilmQHf+g==
dependencies:
apollo-server-caching "^0.5.1"
apollo-server-env "^2.4.3"
apollo-server-env "^2.4.4"
apollo-engine-reporting-protobuf@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.0.tgz#69fae95cdeacb39b9ff06851420cc2c2a7b9fc8b"
integrity sha512-OgMwtLcuL+YAaO2xgkPbnRJnISLDSNE5F11p7oq+1ws+ws71CPfHAthDCxSObCPSALdhsLAGD0v3u3soBuNmMg==
apollo-engine-reporting-protobuf@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.1.tgz#b6e66e6e382f9bcdc2ac8ed168b047eb1470c1a8"
integrity sha512-TSfr9iAaInV8dhXkesdcmqsthRkVcJkzznmiM+1Ob/GScK7r6hBYCjVDt2613EHAg9SUzTOltIKlGD+N+GJRUw==
dependencies:
"@apollo/protobufjs" "^1.0.3"
apollo-engine-reporting@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.8.0.tgz#46a098fe4625b8c9f8866cc9601b3e2ab4ac4db3"
integrity sha512-VPVpIGW6lbYXga6sqq/fG8ZaPR70bFuxvCov6X0npuVQPXwgZrzBp50cHx9uIaBVxDDxD3leeznsQbmF37RAww==
apollo-engine-reporting@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-2.0.0.tgz#af007b4a8a481fa97baef0eac51a7824f1ec3310"
integrity sha512-FvNwORsh3nxEfvQqd2xbd468a0q/R3kYar/Bk6YQdBX5qwqUhqmOcOSxLFk8Zb77HpwHij5CPpPWJb53TU1zcA==
dependencies:
apollo-engine-reporting-protobuf "^0.5.0"
apollo-engine-reporting-protobuf "^0.5.1"
apollo-graphql "^0.4.0"
apollo-server-caching "^0.5.1"
apollo-server-env "^2.4.3"
apollo-server-env "^2.4.4"
apollo-server-errors "^2.4.1"
apollo-server-types "^0.4.0"
apollo-server-plugin-base "^0.9.0"
apollo-server-types "^0.5.0"
async-retry "^1.2.1"
graphql-extensions "^0.12.0"
uuid "^8.0.0"
apollo-env@^0.6.4:
version "0.6.4"
@@ -2021,26 +2022,26 @@ apollo-server-caching@^0.5.1:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.13.0.tgz#d0273781d37d46dca0e96944dd25ac845094b1ec"
integrity sha512-PqfsexbyObaQYb2jODs8v/XzrJcn+5mh0jA8ZfQCg5GENlua/CjeTZbRm2X0p3qpwc2E5jFAXSshrIWvhQAGZQ==
apollo-server-core@^2.14.2:
version "2.14.2"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.14.2.tgz#4ab055b96b8be7821a726c81e8aa412deb7f3644"
integrity sha512-8G6Aoz+k+ecuQco1KNLFbMrxhe/8uR4AOaOYEvT/N5m/6lrkPYzvBAxbpRIub5AxEwpBPcIrI452rR3PD9DItA==
dependencies:
"@apollographql/apollo-tools" "^0.4.3"
"@apollographql/graphql-playground-html" "1.6.24"
"@types/graphql-upload" "^8.0.0"
"@types/ws" "^7.0.0"
apollo-cache-control "^0.10.0"
apollo-datasource "^0.7.0"
apollo-engine-reporting "^1.8.0"
apollo-cache-control "^0.11.0"
apollo-datasource "^0.7.1"
apollo-engine-reporting "^2.0.0"
apollo-server-caching "^0.5.1"
apollo-server-env "^2.4.3"
apollo-server-env "^2.4.4"
apollo-server-errors "^2.4.1"
apollo-server-plugin-base "^0.8.0"
apollo-server-types "^0.4.0"
apollo-tracing "^0.10.0"
apollo-server-plugin-base "^0.9.0"
apollo-server-types "^0.5.0"
apollo-tracing "^0.11.0"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "^0.12.0"
graphql-extensions "^0.12.2"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
@@ -2049,10 +2050,10 @@ apollo-server-core@^2.13.0:
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-env@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
apollo-server-env@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.4.tgz#12d2d0896dcb184478cba066c7a683ab18689ca1"
integrity sha512-c2oddDS3lwAl6QNCIKCLEzt/dF9M3/tjjYRVdxOVN20TidybI7rAbnT4QOzf4tORnGXtiznEAvr/Kc9ahhKADg==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
@@ -2062,10 +2063,10 @@ apollo-server-errors@^2.4.1:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.4.1.tgz#16ad49de6c9134bfb2b7dede9842e73bb239dbe2"
integrity sha512-7oEd6pUxqyWYUbQ9TA8tM0NU/3aGtXSEibo6+txUkuHe7QaxfZ2wHRp+pfT1LC1K3RXYjKj61/C2xEO19s3Kdg==
apollo-server-koa@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/apollo-server-koa/-/apollo-server-koa-2.13.0.tgz#c0bb58881c36ac1925e150180b522c5ea3c31f24"
integrity sha512-tNtDWvJWmOMJRX8Szo7lzWw7K+1l4j4Sc+Y2ZGfr83qSGi0CJM87DoRIPsCNXD2AQwiTJC2f8zKByVpS6NsoFQ==
apollo-server-koa@^2.14.2:
version "2.14.2"
resolved "https://registry.yarnpkg.com/apollo-server-koa/-/apollo-server-koa-2.14.2.tgz#95d098dd6cf831e2e466908956c2dee8b8b6d59b"
integrity sha512-pQuQPQeIkUdsjn3anRN5y1nZSM+tu/H2rCLae3l2gBZknj8ABJ20EeBKFjnB2/6OuHo3FQJSZVt9zeRbMfWKSw==
dependencies:
"@apollographql/graphql-playground-html" "1.6.24"
"@koa/cors" "^2.2.1"
@@ -2076,39 +2077,39 @@ apollo-server-koa@^2.13.0:
"@types/koa-compose" "^3.2.2"
"@types/koa__cors" "^2.2.1"
accepts "^1.3.5"
apollo-server-core "^2.13.0"
apollo-server-types "^0.4.0"
apollo-server-core "^2.14.2"
apollo-server-types "^0.5.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
koa "2.11.0"
koa "2.12.0"
koa-bodyparser "^4.2.1"
koa-compose "^4.1.0"
koa-router "^7.4.0"
type-is "^1.6.16"
apollo-server-plugin-base@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.8.0.tgz#6ab055f5fd18b0c225643aafc46787d622e0a47c"
integrity sha512-H8sJlOVJrF0IhYIFMv7NOgB6BFgqobXSZrj1y9ju6dq13OotsqcZC4fJOYc9oWzb/+/mqg/odtVioE71mj68yg==
apollo-server-plugin-base@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.9.0.tgz#777f720a1ee827a66b8c159073ca30645f8bc625"
integrity sha512-LWcPrsy2+xqwlNseh/QaGa/MPNopS8c4qGgh0g0cAn0lZBRrJ9Yab7dq+iQ6vdUBwIhUWYN6s9dwUWCZw2SL8g==
dependencies:
apollo-server-types "^0.4.0"
apollo-server-types "^0.5.0"
apollo-server-types@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.4.0.tgz#74c2ed18ef46021f83a5a2dc0b844518b13044bf"
integrity sha512-U+6qKCdrucVSMEVvLSqSwxIGr3VI6vcfbhpD86sdb8MgHHGH6egjNAcLrPVRk1AyXs8RV0Ysus+vlj8rpouBzA==
apollo-server-types@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.5.0.tgz#51f39c5fa610ece8b07f1fbcf63c47d4ac150340"
integrity sha512-zhtsqqqfdeoJQAfc41Sy6WnnBVxKNgZ34BKXf/Q+kXmw7rbZ/B5SG3SJMvj1iFsbzZxILmWdUsE9aD20lEr0bg==
dependencies:
apollo-engine-reporting-protobuf "^0.5.0"
apollo-engine-reporting-protobuf "^0.5.1"
apollo-server-caching "^0.5.1"
apollo-server-env "^2.4.3"
apollo-server-env "^2.4.4"
apollo-tracing@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.10.0.tgz#24fc911c0d5145e3ef20a6cdc8d0b807b8cc7eee"
integrity sha512-yuqA1KT0FQUfzVK3ZIk0hRIE8eUKx9Oklq83AGQxLtS/oafBj/VOCZAtJNJkyEqMJxXQT9uIBtbfO1789Gczkw==
apollo-tracing@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.11.0.tgz#8821eb60692f77c06660fb6bc147446f600aecfe"
integrity sha512-I9IFb/8lkBW8ZwOAi4LEojfT7dMfUSkpnV8LHQI8Rcj0HtzL9HObQ3woBmzyGHdGHLFuD/6/VHyFD67SesSrJg==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.12.0"
apollo-server-env "^2.4.4"
apollo-server-plugin-base "^0.9.0"
apollo-utilities@^1.0.1, apollo-utilities@^1.3.0:
version "1.3.3"
@@ -4312,14 +4313,14 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graphql-extensions@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.0.tgz#c0102d7bbe94ad6fc39806fd09bd37868ac928a2"
integrity sha512-kBRLtNeknrFl0W/UQQYebj6qnvb1E1RpQ2+C7Y8pwMc6yV8+9pWFx5RP0HzfeEuScCmK93i3H5sdPedoQWwENw==
graphql-extensions@^0.12.2:
version "0.12.2"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.2.tgz#f22210e812939b7caa2127589f30e6a1c671540f"
integrity sha512-vFaZua5aLiCOOzxfY5qzHZ6S52BCqW7VVOwzvV52Wb5edRm3dn6u+1MR9yYyEqUHSf8LvdhEojYlOkKiaQ4ghA==
dependencies:
"@apollographql/apollo-tools" "^0.4.3"
apollo-server-env "^2.4.3"
apollo-server-types "^0.4.0"
apollo-server-env "^2.4.4"
apollo-server-types "^0.5.0"
graphql-subscriptions@^1.0.0:
version "1.1.0"
@@ -5678,7 +5679,36 @@ koa-router@^8.0.8:
path-to-regexp "1.x"
urijs "^1.19.2"
[email protected]1.0, koa@^2.11.0:
[email protected]2.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.12.0.tgz#c92bfb42defd86f365c31bf63fe918db11fc5c74"
integrity sha512-WlUBj6PXoVhjI5ljMmlyK+eqkbVFW5XQu8twz6bd4WM2E67IwKgPMu5wIFXGxAsZT7sW5xAB54KhY8WAEkLPug==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
cookies "~0.8.0"
debug "~3.1.0"
delegates "^1.0.0"
depd "^1.1.2"
destroy "^1.0.4"
encodeurl "^1.0.2"
escape-html "^1.0.3"
fresh "~0.5.2"
http-assert "^1.3.0"
http-errors "^1.6.3"
is-generator-function "^1.0.7"
koa-compose "^4.1.0"
koa-convert "^1.2.0"
on-finished "^2.3.0"
only "~0.0.2"
parseurl "^1.3.2"
statuses "^1.5.0"
type-is "^1.6.16"
vary "^1.1.2"
koa@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4"
integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==
@@ -8721,6 +8751,11 @@ uuid@^7.0.1, uuid@^7.0.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
uuid@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"