Skip to content

Commit

Permalink
@dandi/model-validation refactor to separate model building from vali…
Browse files Browse the repository at this point in the history
…dation; remove key/object mapping logic from @dandi/data and @dandi/data-pg

supports #13, #18
  • Loading branch information
DanielSchaffer committed Oct 5, 2018
1 parent 8a43972 commit cfe006a
Show file tree
Hide file tree
Showing 91 changed files with 1,431 additions and 1,179 deletions.
4 changes: 2 additions & 2 deletions _examples/simple-express-rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This example demonstrates usage of the following `@dandi` packages:
- `@dandi/core`
- `@dandi/mvc`
- `@dandi/mvc-express`
- `@dandi/model-validation`
- `@dandi/model-builder`

## Running the Example

Expand Down Expand Up @@ -36,7 +36,7 @@ starts the container.
This file is used to construct the `@dandi` DI container used to run
the application. It pulls in the MVC service implementations from
`@dandi/mvc` and `@dandi/mvc-express`, validation implementations from
`@dandi/model-validation`, as well as the controller used in
`@dandi/model-builder`, as well as the controller used in
the application itself.

### src/data/data.model.ts
Expand Down
2 changes: 1 addition & 1 deletion _examples/simple-express-rest-api/src/server.container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CascadingCache, MemoryCache, ServiceContextCacheKeyGenerator } from '@dandi/cache';
import { ConsoleLogger, Container, AmbientInjectableScanner } from '@dandi/core';
import { Validation } from '@dandi/model-validation';
import { Validation } from '@dandi/model-builder';
import { ExpressMvcApplication } from '@dandi/mvc-express';
import { MvcHal } from '@dandi/mvc-hal';

Expand Down
43 changes: 21 additions & 22 deletions aws-lambda-wrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ Lambda service.
Providing functionality for an AWS Lambda function is broken into
several chunks:

* `LambdaEventTransformer` - takes the raw AWS event and converts it
into the desired format to be used by the `LambdaHandler`. Each type
of Lambda event trigger will have its own implementation (e.g.
`HttpEventTransformer`)
* `LambdaHandler` - Implemented by the consumer (developer), contains
the business logic for receiving the transformed event data and doing
any processing.
* `LambdaResponder` - takes the output of the `LambdaHandler` and
converts it into the format expected by AWS Lambda. As with
`LambdaEventTransformer`, each type of Lambda event has its own
implementation (e.g. `HttpResponder`)
- `LambdaEventTransformer` - takes the raw AWS event and converts it
into the desired format to be used by the `LambdaHandler`. Each type
of Lambda event trigger will have its own implementation (e.g.
`HttpEventTransformer`)
- `LambdaHandler` - Implemented by the consumer (developer), contains
the business logic for receiving the transformed event data and doing
any processing.
- `LambdaResponder` - takes the output of the `LambdaHandler` and
converts it into the format expected by AWS Lambda. As with
`LambdaEventTransformer`, each type of Lambda event has its own
implementation (e.g. `HttpResponder`)

The transformer and responder implementations are grouped into modules
to make them easy to set up.
Expand All @@ -27,19 +27,18 @@ to make them easy to set up.

There are 2 pieces of logic required to set up a Lambda function:

* Your handler implementation - an implementation of `LambdaHandler<TEventData>`
* A "main" file using the `Lambda` helper, which references your
`LambdaHandler` implementation, as well as a module containing the
`LambdaEventTransformer` and `LambdaResponder` implementations required
for the type of events that will be handled.
- Your handler implementation - an implementation of `LambdaHandler<TEventData>`
- A "main" file using the `Lambda` helper, which references your
`LambdaHandler` implementation, as well as a module containing the
`LambdaEventTransformer` and `LambdaResponder` implementations required
for the type of events that will be handled.

## API Gateway/HTTP Events

`LambdaEventTransformer` and `LambdaResponder` implementations for
API Gateway proxied events are provided in the `AwsLambdaHttpModule`:

```typescript

// my-handler.ts
import { HttpHandlerRequest, LambdaHandler } from '@dandi/aws-lambda';
import { Context } from 'aws-lambda';
Expand All @@ -61,10 +60,10 @@ export handler = Lambda.handler(MyHandler, AwsLambdaHttpModule);
### Interceptors

Implementations of `HttpResponseInterceptor` can be used to modify
the response sent by the Lambda function. This can be used, for
example, to add extra headers or modify the body or statusCode.
Interceptors can be enabled by adding their classes to the
`Lambda.handler()` call:
the response sent by the Lambda function. This can be used, for
example, to add extra headers or modify the body or statusCode.
Interceptors can be enabled by adding their classes to the
`Lambda.handler()` call:

```typescript
// my-interceptor.ts
Expand Down Expand Up @@ -112,7 +111,7 @@ export handler = Lambda.handler(MyHandler, AwsLambdaHttpModule.configure({
### Model Validation

`AwsLambdaHttpModule` can be configured to use model validation features
from `@dandi/model` and `@dandi/model-validation`:
from `@dandi/model` and `@dandi/model-builder`:

```typescript
// my-model.ts
Expand Down
14 changes: 7 additions & 7 deletions aws-lambda-wrap/src/http.event.transformer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { stubProvider, testHarness } from '@dandi/core-testing';
import { DecoratorModelValidator, ModelValidator } from '@dandi/model-validation';
import { DecoratorModelBuilder, ModelBuilder } from '@dandi/model-builder';

import { APIGatewayProxyEvent, Context } from 'aws-lambda';

Expand Down Expand Up @@ -139,32 +139,32 @@ describe('HttpEventTransformer', () => {
});

describe('body validation', () => {
const harness = testHarness(HttpEventTransformer, stubProvider(DecoratorModelValidator, ModelValidator), {
const harness = testHarness(HttpEventTransformer, stubProvider(DecoratorModelBuilder, ModelBuilder), {
provide: HttpEventOptions,
useValue: {
validateBody: TestBody,
},
});

let validator: SinonStubbedInstance<ModelValidator>;
let builder: SinonStubbedInstance<ModelBuilder>;

beforeEach(async () => {
transformer = await harness.inject(LambdaEventTransformer);
validator = await harness.injectStub(ModelValidator);
builder = await harness.injectStub(ModelBuilder);

validator.validateModel.returns(body);
builder.constructModel.returns(body);
});
afterEach(() => {
transformer = undefined;
validator = undefined;
builder = undefined;
});

it('validates the body and creates a HttpHandlerRequest object using the event values and deserialized body', () => {
const eventWithoutBody = Object.assign({}, event);
delete eventWithoutBody.body;

const result = transformer.transform(event, context);
expect(validator.validateModel).to.have.been.calledWith(TestBody, body);
expect(builder.constructModel).to.have.been.calledWith(TestBody, body);

expect(result).to.include(eventWithoutBody);
expect(result.body).to.deep.equal(body);
Expand Down
12 changes: 6 additions & 6 deletions aws-lambda-wrap/src/http.event.transformer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable, Optional } from '@dandi/core';
import { ModelValidator } from '@dandi/model-validation';
import { ModelBuilder } from '@dandi/model-builder';

import { APIGatewayEventRequestContext, APIGatewayProxyEvent, Context } from 'aws-lambda';

Expand Down Expand Up @@ -28,9 +28,9 @@ export class HttpEventTransformer implements LambdaEventTransformer<APIGatewayPr
@Inject(HttpEventOptions)
@Optional()
private options: HttpEventOptions,
@Inject(ModelValidator)
@Inject(ModelBuilder)
@Optional()
private validator: ModelValidator,
private builder: ModelBuilder,
) {}

public transform(event: APIGatewayProxyEvent, context: Context): HttpHandlerRequest {
Expand All @@ -44,11 +44,11 @@ export class HttpEventTransformer implements LambdaEventTransformer<APIGatewayPr
}

if (this.options && this.options.validateBody) {
if (!this.validator) {
throw new DandiAwsLambdaError('validateBody option is set, but no ModelValidator is provided');
if (!this.builder) {
throw new DandiAwsLambdaError('validateBody option is set, but no ModelBuilder is provided');
}

body = this.validator.validateModel(this.options.validateBody, body);
body = this.builder.constructModel(this.options.validateBody, body);
}

return {
Expand Down
6 changes: 3 additions & 3 deletions config/src/config.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { isPrimitiveType } from '@dandi/common';
import { Inject, Injectable } from '@dandi/core';
import { ModelValidator } from '@dandi/model-validation';
import { ModelBuilder } from '@dandi/model-builder';

import { ConfigClient, isAsyncConfigClient } from './config.client';
import { ConfigToken } from './config.token';
import { InvalidConfigClientError } from './invalid.config.client.error';

@Injectable()
export class ConfigResolver {
constructor(@Inject(ModelValidator) private validator: ModelValidator) {}
constructor(@Inject(ModelValidator) private validator: ModelBuilder) {}

public async resolve<T>(client: ConfigClient, token: ConfigToken<T>): Promise<T> {
if (token.encrypted && !client.allowsEncryption) {
Expand All @@ -20,6 +20,6 @@ export class ConfigResolver {

const strValue: string = isAsyncConfigClient(client) ? await client.get(token) : client.get(token);
const value = isPrimitiveType(token.type) ? strValue : JSON.parse(strValue);
return this.validator.validateModel(token.type, value);
return this.validator.constructModel(token.type, value);
}
}
2 changes: 1 addition & 1 deletion data-pg/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './src/pg.db';
export * from './src/pg.db.module';
export * from './src/pg.db.client';
export * from './src/pg.db.config';
export * from './src/pg.db.pool';
Expand Down
19 changes: 6 additions & 13 deletions data-pg/src/pg.db.client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { AppError } from '@dandi/common';
import { Container, Logger, NoopLogger, Resolver } from '@dandi/core';
import { DataMapper, PassThroughDataMapper } from '@dandi/data';
import { ModelValidator } from '@dandi/model-validation';
import { DecoratorModelValidator } from '@dandi/model-validation/src/decorator.model.validator';
import { DecoratorModelBuilder, ModelBuilder } from '@dandi/model-builder';
import { PgDbClient, PgDbPool, TransactionAlreadyInProgressError } from '@dandi/data-pg';

import { expect } from 'chai';
import { Pool, PoolClient } from 'pg';
import { PoolClient } from 'pg';
import { createStubInstance, SinonStub, SinonStubbedInstance, stub } from 'sinon';

import { PgDbClient, TransactionAlreadyInProgressError } from './pg.db.client';
import { PgDbPool } from './pg.db.pool';

describe('PgDbClient', () => {
let pool: PgDbPool & SinonStubbedInstance<PgDbPool>;
let poolClient: SinonStubbedInstance<PoolClient>;
let dataMapper: SinonStubbedInstance<DataMapper>;
let modelValidator: SinonStubbedInstance<ModelValidator>;
let modelValidator: SinonStubbedInstance<ModelBuilder>;
let resolver: SinonStubbedInstance<Resolver>;
let logger: SinonStubbedInstance<Logger>;
let dbClient: PgDbClient;
Expand All @@ -25,16 +20,14 @@ describe('PgDbClient', () => {
query: stub().returns({ rows: [] }),
release: stub(),
} as any;
dataMapper = createStubInstance(PassThroughDataMapper);
modelValidator = createStubInstance(DecoratorModelValidator);
modelValidator = createStubInstance(DecoratorModelBuilder);
resolver = createStubInstance(Container);
logger = createStubInstance(NoopLogger);
dbClient = new PgDbClient(pool, dataMapper, modelValidator, resolver, logger);
dbClient = new PgDbClient(pool, modelValidator, resolver, logger);
});
afterEach(() => {
pool = undefined;
poolClient = undefined;
dataMapper = undefined;
modelValidator = undefined;
resolver = undefined;
logger = undefined;
Expand Down
9 changes: 4 additions & 5 deletions data-pg/src/pg.db.client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AppError, Disposable } from '@dandi/common';
import { Inject, Injectable, Logger, Resolver } from '@dandi/core';
import { DataMapper, DbClient, DbTransactionClient, TransactionFn } from '@dandi/data';
import { ModelValidator } from '@dandi/model-validation';
import { DbClient, DbTransactionClient, TransactionFn } from '@dandi/data';
import { ModelBuilder } from '@dandi/model-builder';

import { PgDbPool } from './pg.db.pool';
import { PgDbQueryableBase } from './pg.db.queryable';
Expand All @@ -18,12 +18,11 @@ export class PgDbClient extends PgDbQueryableBase<PgDbPool> implements DbClient,

constructor(
@Inject(PgDbPool) pool: PgDbPool,
@Inject(DataMapper) dataMapper: DataMapper,
@Inject(ModelValidator) modelValidator: ModelValidator,
@Inject(ModelBuilder) modelValidator: ModelBuilder,
@Inject(Resolver) private resolver: Resolver,
@Inject(Logger) private logger: Logger,
) {
super(pool, dataMapper, modelValidator);
super(pool, modelValidator);
}

public async transaction<T>(transactionFn: TransactionFn<T>): Promise<T> {
Expand Down
7 changes: 7 additions & 0 deletions data-pg/src/pg.db.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PgDbPool } from './pg.db.pool';
import { PgDbPoolConfig } from './pg.db.config';
import { PgDbClient } from './pg.db.client';
import { POOL_CLIENT_PROVIDER } from './pg.db.pool.client';
import { PgDbTransactionClient } from './pg.db.transaction.client';

export const PgDbModule = [PgDbPool, PgDbPoolConfig, PgDbClient, PgDbTransactionClient, POOL_CLIENT_PROVIDER];
29 changes: 6 additions & 23 deletions data-pg/src/pg.db.queryable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import { DataMapper, PassThroughDataMapper } from '@dandi/data';
import { ModelValidator } from '@dandi/model-validation';
import { ModelBuilder } from '@dandi/model-builder';

import { expect } from 'chai';
import { createStubInstance, SinonStubbedInstance, stub } from 'sinon';
import { SinonStubbedInstance, stub } from 'sinon';

import { PgDbQueryableBase, PgDbQueryableClient } from './pg.db.queryable';

describe('PgDbQueryableBase', () => {
let client: SinonStubbedInstance<PgDbQueryableClient>;
let dataMapper: SinonStubbedInstance<DataMapper>;
let queryable: PgDbQueryableBase<PgDbQueryableClient>;
let modelValidator: ModelValidator;
let modelValidator: ModelBuilder;
let clientResult: any;

beforeEach(() => {
clientResult = { rows: [{ id: 'a' }, { id: 'b' }] };
client = {
query: stub().returns(clientResult),
};
dataMapper = createStubInstance(PassThroughDataMapper);
modelValidator = {
validateMember: stub(),
validateModel: stub(),
constructMember: stub(),
constructModel: stub(),
};
queryable = new PgDbQueryableBase<PgDbQueryableClient>(client, dataMapper, modelValidator);
queryable = new PgDbQueryableBase<PgDbQueryableClient>(client, modelValidator);
});
afterEach(() => {
client = undefined;
dataMapper = undefined;
modelValidator = undefined;
queryable = undefined;
});
Expand All @@ -41,18 +37,5 @@ describe('PgDbQueryableBase', () => {

expect(client.query).to.have.been.calledWithExactly(cmd, [args]);
});

it('returns the result of passing the rows property through dataMapper.mapFromDb()', async () => {
const value = { id: 'c' };
dataMapper.mapFromDb.returns(value);

const cmd = 'SELECT foo FROM bar WHERE ix = $1';
const args = ['nay'];

const result = await queryable.query(cmd, args);

expect(dataMapper.mapFromDb).to.have.been.calledWith(clientResult.rows[0]);
expect(result).to.deep.equal([value, value]);
});
});
});
10 changes: 5 additions & 5 deletions data-pg/src/pg.db.queryable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Constructor, Url, Uuid } from '@dandi/common';
import { DataMapper, DbQueryable } from '@dandi/data';
import { DbQueryable } from '@dandi/data';
import { DataPropertyMetadata, ModelUtil } from '@dandi/model';
import { ModelValidator } from '@dandi/model-validation';
import { ModelBuilder } from '@dandi/model-builder';

import { snakeCase } from 'change-case';
import { QueryResult } from 'pg';
Expand All @@ -13,7 +13,7 @@ export interface PgDbQueryableClient {
}

export class PgDbQueryableBase<TClient extends PgDbQueryableClient> implements DbQueryable {
constructor(protected client: TClient, protected dataMapper: DataMapper, protected modelValidator: ModelValidator) {}
constructor(protected client: TClient, protected modelBuilder: ModelBuilder) {}

public async query(cmd: string, ...args: any[]): Promise<any[]> {
let result: QueryResult;
Expand All @@ -27,7 +27,7 @@ export class PgDbQueryableBase<TClient extends PgDbQueryableClient> implements D
} catch (err) {
throw new PgDbQueryError(err);
}
return result.rows.map((row: any) => this.dataMapper.mapFromDb(row));
return result.rows;
}

public async queryModel<T>(model: Constructor<T>, cmd: string, ...args: any[]): Promise<T[]> {
Expand All @@ -36,7 +36,7 @@ export class PgDbQueryableBase<TClient extends PgDbQueryableClient> implements D
if (!result || !result.length) {
return result;
}
return result.map((item) => this.modelValidator.validateModel(model, item));
return result.map((item) => this.modelBuilder.constructModel(model, item));
}

public async queryModelSingle<T>(model: Constructor<T>, cmd: string, ...args: any[]): Promise<T> {
Expand Down
Loading

0 comments on commit cfe006a

Please sign in to comment.