Skip to content

Commit

Permalink
scratch - model validation error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielSchaffer committed Mar 29, 2020
1 parent acea64a commit d45e1e3
Show file tree
Hide file tree
Showing 87 changed files with 2,367 additions and 756 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
@@ -1 +1 @@
12.14.1
12.16.1
@@ -1,4 +1,4 @@
import { RequestBody } from '@dandi/http-model'
import { RequestModel } from '@dandi/http-model'
import { Property, Required } from '@dandi/model'
import { Controller, HttpGet, HttpPost } from '@dandi/mvc'
import { View } from '@dandi/mvc-view'
Expand Down Expand Up @@ -26,7 +26,7 @@ export class HyperviewController {

@HttpPost('detail.xml')
@View('detail.pug', { xml: true })
public detail(@RequestBody(FormModel) form: FormModel): any {
public detail(@RequestModel(FormModel) form: FormModel): any {
return form
}

Expand Down
@@ -1,6 +1,6 @@
import { Uuid } from '@dandi/common'
import { Inject } from '@dandi/core'
import { PathParam, RequestBody } from '@dandi/http-model'
import { PathParam, RequestModel } from '@dandi/http-model'
import { Controller, Cors, HttpGet, HttpPost, HttpPut } from '@dandi/mvc'
import { AccessorResourceId, ResourceAccessor, ResourceListAccessor } from '@dandi/mvc-hal'

Expand All @@ -22,15 +22,15 @@ export class ListController {
}

@HttpPost()
public async addList(@RequestBody(ListRequest) listRequest): Promise<ListResource> {
public async addList(@RequestModel(ListRequest) listRequest): Promise<ListResource> {
return new ListResource(await this.listManager.addList(listRequest))
}

@HttpPut()
@Cors({
allowOrigin: ['this-should-never-get accessed-via-cors'],
})
public async putList(@RequestBody(ListRequest) listRequest): Promise<ListResource> {
public async putList(@RequestModel(ListRequest) listRequest): Promise<ListResource> {
return new ListResource(await this.listManager.addList(listRequest))
}

Expand All @@ -55,7 +55,7 @@ export class ListController {
}

@HttpPost(':listId/task')
public addTask(@PathParam(Uuid) listId, @RequestBody(TaskRequest) taskRequest): Promise<Task> {
public addTask(@PathParam(Uuid) listId, @RequestModel(TaskRequest) taskRequest): Promise<Task> {
return this.listManager.addTask(listId, taskRequest)
}
}
@@ -1,6 +1,6 @@
import { Uuid } from '@dandi/common'
import { Inject } from '@dandi/core'
import { PathParam, RequestBody } from '@dandi/http-model'
import { PathParam, RequestModel } from '@dandi/http-model'
import { Controller, HttpGet, HttpPatch } from '@dandi/mvc'
import { AccessorResourceId, ResourceAccessor } from '@dandi/mvc-hal'

Expand All @@ -22,7 +22,7 @@ export class TaskController {
}

@HttpPatch(':taskId')
public async updateTask(@PathParam(Uuid) taskId, @RequestBody(Task) task): Promise<TaskResource> {
public async updateTask(@PathParam(Uuid) taskId, @RequestModel(Task) task): Promise<TaskResource> {
if (taskId !== task.taskId) {
throw new Error('taskId on path did not match taskId on model')
}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -33,7 +33,7 @@
"@types/chai-as-promised": "^7.1.2",
"@types/colors": "^1.2.1",
"@types/luxon": "^1.15.2",
"@types/mocha": "^5.2.7",
"@types/mocha": "7.0.2",
"@types/node": "^12.12.24",
"@types/sinon-chai": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^2.19.2",
Expand All @@ -52,12 +52,12 @@
"nyc": "^15.0.0",
"rimraf": "^3.0.0",
"rxjs": "^6.4.0",
"sinon": "^8.0.2",
"sinon": "^9.0.1",
"sinon-chai": "^3.4.0",
"ts-custom-error-shim": "^1.0.2",
"ts-node": "^8.5.4",
"tsconfig-paths": "^3.9.0",
"typescript": "^3.7.4",
"uuid": "^3.3.3"
"uuid": "^7.0.2"
}
}
Expand Up @@ -2,7 +2,7 @@ import { AwsLambdaHttpModule, LambdaHandler, LambdaHandlerFn, Lambda } from '@da
import { APIGatewayProxyEvent } from '@dandi-contrib/aws-lambda/node_modules/@types/aws-lambda'
import { Injectable } from '@dandi/core'
import { HttpHeader, HttpMethod, HttpModule, HttpStatusCode, MimeType } from '@dandi/http'
import { QueryParam, RequestBody } from '@dandi/http-model'
import { QueryParam, RequestModel } from '@dandi/http-model'
import { HttpPipelineModule, NativeJsonObjectRenderer } from '@dandi/http-pipeline'
import { Property, Required } from '@dandi/model'
import { ModelBuilderModule } from '@dandi/model-builder'
Expand Down Expand Up @@ -34,7 +34,7 @@ describe('AWS Lambda Http Events', () => {
@Injectable()
class TestPostHandler implements LambdaHandler {

public handleEvent(@RequestBody(TestModel) model: TestModel): any {
public handleEvent(@RequestModel(TestModel) model: TestModel): any {
return {
message: model.message,
}
Expand Down
5 changes: 1 addition & 4 deletions packages/dandi-contrib/aws-lambda/src/lambda.ts
Expand Up @@ -9,7 +9,6 @@ import {
Provider,
Registerable,
} from '@dandi/core'
import { createHttpRequestScope } from '@dandi/http'
import {
DefaultHttpRequestInfo,
HttpPipeline,
Expand Down Expand Up @@ -74,9 +73,7 @@ export class Lambda<TEvent, TEventData, THandler extends LambdaHandler> {

public async handleEvent(event: TEvent, context: Context): Promise<any> {
const providers = this.createProviders(event, context)
return Disposable.useAsync(this.injector.createChild(createHttpRequestScope(event as any), providers), async (injector) => {
return await injector.invoke(this.httpPipeline, 'handleRequest', ...providers)
})
return await this.injector.invoke(this.httpPipeline, 'handleRequest', ...providers)
}

private createProviders(event: TEvent, context: Context): Provider<any>[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/common/index.ts
Expand Up @@ -11,7 +11,7 @@ export * from './src/invalid-access-error'
export * from './src/error.util'
export * from './src/jsonable'
export * from './src/metadata'
export * from './src/method.target'
export * from './src/method-target'
export * from './src/package.global.symbol'
export * from './src/parse-boolean'
export * from './src/primitive'
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/common/package.json
Expand Up @@ -2,6 +2,6 @@
"name": "@dandi/common",
"peerDependencies": {
"luxon": "^1.4.3",
"uuid": "^3.3.2"
"uuid": "^7.0.0"
}
}
10 changes: 10 additions & 0 deletions packages/dandi/common/src/constructor.ts
Expand Up @@ -8,9 +8,19 @@ export type PrimitiveConstructor<T extends boolean | number | string> =
T extends string ? StringConstructor :
never

// const IS_CONSTRUCTOR_PATTERN = /^class\s+\w+\s*\{/
// const IS_CONSTRUCTOR = globalSymbol('IS_CONSTRUCTOR')

export function isConstructor<T>(obj: any): obj is Constructor<T> {
if (typeof obj !== 'function') {
return false
}
return !!obj.prototype
// const cachedResult = Reflect.get(obj, IS_CONSTRUCTOR)
// if (typeof cachedResult !== 'undefined') {
// return cachedResult
// }
// const isConstructor = IS_CONSTRUCTOR_PATTERN.test(obj.name)
// Reflect.set(obj, IS_CONSTRUCTOR, isConstructor)
// return isConstructor
}
Expand Up @@ -2,6 +2,6 @@ import { Constructor } from './constructor'

export type ClassMethods<T> = { [TProp in keyof T]?: T[TProp] }

export type MethodTarget<T> = ClassMethods<T> & {
export type MethodTarget<T = any> = ClassMethods<T> & {
constructor: Constructor<T>
}
2 changes: 1 addition & 1 deletion packages/dandi/common/src/uuid.ts
@@ -1,4 +1,4 @@
import * as uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'

const UUID = new Map<string, Uuid>()

Expand Down
1 change: 1 addition & 0 deletions packages/dandi/core/decorators/index.ts
@@ -1,3 +1,4 @@
export * from './src/inject-decorator'
export * from './src/injectable-decorator'
export * from './src/optional-decorator'
export * from './src/scope-decorator'
12 changes: 12 additions & 0 deletions packages/dandi/core/decorators/src/scope-decorator.ts
@@ -0,0 +1,12 @@
import { CreateScopeFn, getInjectableMetadata } from '@dandi/core/internal/util'

export function Scope(scopeFn: CreateScopeFn): ClassDecorator & MethodDecorator {
return function scopeDecorator(target: any, propertyKey?: string): void {
const metaTarget = propertyKey ? target[propertyKey] : target.constructor
const meta = getInjectableMetadata(metaTarget)
if (meta.scopeFn && meta.scopeFn !== scopeFn) {
throw Error(`Decorator Error: ${metaTarget} already has scopeFn ${scopeFn} defined`)
}
meta.scopeFn = scopeFn
}
}
1 change: 1 addition & 0 deletions packages/dandi/core/errors/index.ts
@@ -1,4 +1,5 @@
export * from './src/dandi-application-error'
export * from './src/dandi-injection-error'
export * from './src/metadata-provider-error'
export * from './src/missing-provider-error'
export * from './src/provider-type-error'
17 changes: 16 additions & 1 deletion packages/dandi/core/errors/src/dandi-injection-error.ts
@@ -1,14 +1,29 @@
import { AppError, CUSTOM_INSPECTOR } from '@dandi/common'
import { AppError, Constructor, CUSTOM_INSPECTOR } from '@dandi/common'
import { InjectionToken, InjectorContext } from '@dandi/core/types'

import { getTokenString } from '../../internal/util/src/injection-token-util'
import { globalSymbol } from '../../src/global-symbol'

const WRAP_INJECTION_ERROR = globalSymbol('WRAP_INJECTION_ERROR')

export class DandiInjectionError<T> extends AppError {

public static doNotWrap(errClass: Constructor<AppError>): void {
Object.defineProperty(errClass.prototype, WRAP_INJECTION_ERROR, {
value: false,
configurable: false,
})
}

public static shouldWrap(err: any): boolean {
return !(err instanceof DandiInjectionError) && err[WRAP_INJECTION_ERROR] !== false
}

/**
* @internal
*/
constructor(public readonly token: InjectionToken<T>, public readonly context: InjectorContext, messageStart: string, innerError?: Error) {
super(`${messageStart} ${getTokenString(token)} \nfor ${context[CUSTOM_INSPECTOR]()}`, innerError)
}
}
DandiInjectionError.doNotWrap(DandiInjectionError)
7 changes: 7 additions & 0 deletions packages/dandi/core/errors/src/metadata-provider-error.ts
@@ -0,0 +1,7 @@
import { AppError } from '@dandi/common'

export class MetadataProviderError extends AppError {
constructor(message: string, innerError?: Error) {
super(message, innerError)
}
}
4 changes: 3 additions & 1 deletion packages/dandi/core/internal/src/dandi-generator.ts
Expand Up @@ -15,11 +15,12 @@ import {
FactoryParamInjectionScope,
FactoryProvider,
GeneratingProvider,
InjectionScope,
Injector,
InjectorContext,
InstanceGenerator,
Provider,
ResolverContext,
InjectionScope, InjectorContext,
} from '@dandi/core/types'

import { DandiResolverContext } from './dandi-resolver-context'
Expand Down Expand Up @@ -80,6 +81,7 @@ export class DandiGenerator implements InstanceGenerator {
? await Promise.all(
provider.deps.map(async (paramToken) => {
const paramScope: FactoryParamInjectionScope = {
// target: provider,
target: provider,
paramToken,
}
Expand Down
47 changes: 41 additions & 6 deletions packages/dandi/core/internal/src/dandi-injector.ts
@@ -1,10 +1,11 @@
import { Disposable } from '@dandi/common'
import {
DandiInjectionError,
MissingTokenError,
InvalidTokenError,
InvalidTokenScopeError,
MetadataProviderError,
MissingProviderError,
MissingTokenError,
} from '@dandi/core/errors'
import {
getInjectableMetadata,
Expand All @@ -14,6 +15,7 @@ import {
getScopeRestriction,
isInjectionToken,
scopesAreCompatible,
ParamMetadata,
} from '@dandi/core/internal/util'
import {
DependencyInjectionScope,
Expand All @@ -28,6 +30,7 @@ import {
InvokableFn,
InvokeInjectionScope,
OpinionatedToken,
Provider,
Registerable,
ResolvedProvider,
ResolverContext,
Expand All @@ -51,6 +54,7 @@ type KnownArgs<T> = { [TProp in keyof Args<T>]?: Args<T>[TProp] }
export class DandiInjector implements Injector, Disposable {

public readonly context: DandiInjectorContext
public readonly app: Injector

protected generator: InstanceGenerator
protected readonly generatorReady: Promise<void>
Expand All @@ -72,7 +76,9 @@ export class DandiInjector implements Injector, Disposable {
},
...providers || [],
)
this.app = parent.app || this
}

this.generatorReady = this.initGeneratorFactory(generatorFactory)
}

Expand Down Expand Up @@ -100,10 +106,10 @@ export class DandiInjector implements Injector, Disposable {
}
return resolverContext.resolveValue(result)
} catch (err) {
if (err instanceof DandiInjectionError) {
throw err
if (DandiInjectionError.shouldWrap(err)) {
throw new DandiInjectionError(token, this.context, `${err.message} while injecting`, err)
}
throw new DandiInjectionError(token, this.context, `${err.message} while injecting`, err)
throw err
}
}

Expand All @@ -112,8 +118,13 @@ export class DandiInjector implements Injector, Disposable {
methodName: InstanceInvokableFn<TInstance, TResult>,
...providers: Registerable[]
): Promise<TResult> {
const invokeInjectionScope: InvokeInjectionScope = { instance, methodName: methodName.toString() }
return await Disposable.useAsync(this.createChild(invokeInjectionScope, providers), async (injector) => {
const methodMeta = getInjectableMetadata(instance[methodName.toString()])
const invokeInjectionScope: InvokeInjectionScope = methodMeta.scopeFn ?
methodMeta.scopeFn(instance, methodName) :
{ instance, methodName: methodName.toString() }
const methodProviders = this.getMethodProviders(instance, methodName.toString())
const scopeProviders = providers.concat(methodProviders)
return await Disposable.useAsync(this.createChild(invokeInjectionScope, scopeProviders), async (injector) => {
return await injector.invokeInternal(instance, methodName)
})
}
Expand Down Expand Up @@ -157,6 +168,30 @@ export class DandiInjector implements Injector, Disposable {
return await method.apply(instance, invokeTargetArgs)
}

private getMethodProviders(instance: any, methodName: string): Provider<any>[] {
const method = instance[methodName]
const meta = getInjectableMetadata(method)
const methodProviders = meta.params.reduce((result, paramMeta) => {
if (paramMeta.methodProviders) {
paramMeta.methodProviders.forEach(provider => {
const existing = result.get(provider.provide)
if (existing) {
// FIXME: allow multi providers/tokens
const [originalParam] = existing
throw new MetadataProviderError(
`Parameter ${paramMeta.name} of ${instance.constructor.name}.${methodName} is attempting to ` +
`register a provider for token ${provider.provide}, but it is already registered by param ` +
`${originalParam.name}`,
)
}
result.set(provider.provide, [paramMeta, provider])
})
}
return result
}, new Map<InjectionToken<any>, [ParamMetadata<any>, Provider<any>]>())
return [...methodProviders.values()].map(([, provider]) => provider)
}

private async initGeneratorFactory(generatorFactory: InstanceGeneratorFactory): Promise<void> {
this.generator = await (typeof generatorFactory === 'function' ? generatorFactory() : generatorFactory)
}
Expand Down

0 comments on commit d45e1e3

Please sign in to comment.