Skip to content

Commit

Permalink
Merge remote-tracking branch 'master' into 'build/nx'
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-sa committed Oct 2, 2023
2 parents 0e1eccd + a47111c commit 3575be3
Show file tree
Hide file tree
Showing 48 changed files with 885 additions and 87 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@ npm init @deepkit/app my-deepkit-app
- [Serverless Adapter](https://github.com/H4ad/serverless-adapter): Run Deepkit on top of AWS Lambda, Azure, Digital Ocean and many other clouds.
- [Deepkit REST](https://github.com/deepkit-rest/rest): DeepKit REST opens up a whole new declarative and extensive approach for developing REST APIs.
- [Deepkit Stripe](https://github.com/deepkit-community/modules/tree/master/packages/stripe): Interacting with the Stripe API or consuming Stripe webhooks in your Deepkit applications is now easy as pie 🥧.
- [Deepkit GraphQL](https://github.com/marcus-sa/deepkit-modules/tree/main/packages/graphql): Create GraphQL servers using Deepkit
- [Deepkit Apollo Server](https://github.com/marcus-sa/deepkit-modules/tree/main/packages/apollo-graphql): Run your Deepkit GraphQL server using Apollo
- [Deepkit Remix](https://github.com/marcus-sa/deepkit-modules/tree/main/packages/remix): Create Remix apps using Deepkit as the server
- [Deepkit Remix Validated Form](https://github.com/marcus-sa/deepkit-modules/tree/main/packages/remix-validated-form): Use Deepkit Type as form validator for Remix

## Examples

- [Deepkit example with HTTP, RPC, and CLI controller](https://github.com/deepkit/deepkit-framework/blob/master/packages/example-app/app.ts).
- [Deepkit HTTP router with custom http server](https://github.com/deepkit/deepkit-framework/blob/master/packages/example-app/slim.ts).
- [Deepkit Bookstore](https://github.com/marcj/deepkit-bookstore): Auto REST CRUD + Deepkit API Console.
- [Deepkit Webpack](https://github.com/marcj/deepkit-webpack): Type Compiler with Webpack.
- [Deepkit GraphQL](https://github.com/marcus-sa/deepkit-modules/tree/main/apps/example-graphql): Deepkit GraphQL server application with ORM integration
- [Deepkit Remix](https://github.com/marcus-sa/deepkit-modules/tree/main/apps/example-remix): Remix application using Deepkit as the server

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export class App<T extends RootModuleDefinition> {
return this;
}

public async dispatch<T extends EventToken<any>>(eventToken: T, event: EventOfEventToken<T>, injector?: InjectorContext): Promise<void> {
public async dispatch<T extends EventToken<any>>(eventToken: T, event?: EventOfEventToken<T>, injector?: InjectorContext): Promise<void> {
return await this.get(EventDispatcher).dispatch(eventToken, event, injector);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* You should have received a copy of the MIT License along with this program.
*/

import { InjectorModule, ProviderWithScope, Token } from '@deepkit/injector';
import { InjectorModule, ProviderWithScope, Token, NormalizedProvider } from '@deepkit/injector';
import { AbstractClassType, ClassType, CustomError, ExtractClassType, isClass } from '@deepkit/core';
import { EventListener, EventToken } from '@deepkit/event';
import { WorkflowDefinition } from '@deepkit/workflow';
Expand All @@ -23,7 +23,7 @@ export interface MiddlewareConfig {

export type MiddlewareFactory = () => MiddlewareConfig;

export type ExportType = AbstractClassType | string | AppModule<any> | Type;
export type ExportType = AbstractClassType | string | AppModule<any> | Type | NormalizedProvider;

/**
* @reflection never
Expand Down
18 changes: 17 additions & 1 deletion packages/app/tests/module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from '@jest/globals';
import { Minimum, MinLength } from '@deepkit/type';
import { injectorReference } from '@deepkit/injector';
import { injectorReference, provide } from '@deepkit/injector';
import { ServiceContainer } from '../src/lib/service-container.js';
import { ClassType } from '@deepkit/core';
import { AppModule, createModule } from '../src/lib/module.js';
Expand Down Expand Up @@ -248,6 +248,22 @@ test('same module loaded twice', () => {
}
});

test('interface provider can be exported', () => {
interface Test {}

const TEST = {};

const Test = provide<Test>({ useValue: TEST });

const test = new AppModule({ providers: [Test], exports: [Test] });

const app = new AppModule({ imports: [test] });

const serviceContainer = new ServiceContainer(app);

expect(serviceContainer.getInjector(app).get<Test>()).toBe(TEST);
});

test('non-exported providers can not be overwritten', () => {
class SubClass {
}
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,42 @@ export function zip<T extends (readonly unknown[])[]>(
//@ts-ignore
return Array.from({ length: minLength }).map((_, i) => args.map((arr) => arr[i]));
}

/**
* Forwards the runtime type arguments from function x to function y.
* This is necessary when a generic function is overridden and forwarded to something else.
*
* ```typescript
* let generic = <T>(type?: ReceiveType<T>) => undefined;
*
* let forwarded<T> = () => {
* forwardTypeArguments(forwarded, generic); //all type arguments are forwarded to generic()
* generic(); //call as usual
* }
*
* forwarded<any>(); //generic receives any in runtime.
* ```
*
* Note that generic.bind(this) will not work, as bind() creates a new function and forwarded type arguments can not
* reach the original function anymore.
*
* ```typescript
* let forwarded<T> = () => {
* const bound = generic.bind(this);
* forwardTypeArguments(forwarded, bound); //can not be forwarded anymore
* bound(); //fails
* }
* ```
*
* This is a limitation of JavaScript. In this case you have to manually forward type arguments.
*
* ```typescript
* let forwarded<T> = (type?: ReceiveType<T>) => {
* const bound = generic.bind(this);
* bound(type);
* }
* ```
*/
export function forwardTypeArguments(x: any, y: any): void {
y.Ω = x.Ω;
}
3 changes: 3 additions & 0 deletions packages/framework/src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ApiConsoleModule } from '@deepkit/api-console-module';
import { AppModule, ControllerConfig, createModule } from '@deepkit/app';
import { RpcControllers, RpcInjectorContext, RpcKernelWithStopwatch } from './rpc.js';
import { normalizeDirectory } from './utils.js';
import { ScopedLogger } from "@deepkit/logger";

export class FrameworkModule extends createModule({
config: FrameworkConfig,
Expand All @@ -51,6 +52,7 @@ export class FrameworkModule extends createModule({
RpcServer,
ConsoleTransport,
Logger,
ScopedLogger,
MigrationProvider,
DebugController,
{ provide: DatabaseRegistry, useFactory: (ic: InjectorContext) => new DatabaseRegistry(ic) },
Expand Down Expand Up @@ -106,6 +108,7 @@ export class FrameworkModule extends createModule({
MigrationCreateController,
],
exports: [
ScopedLogger.provide,
ProcessLocker,
ApplicationServer,
WebWorkerFactory,
Expand Down
1 change: 1 addition & 0 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './lib/decorator.js';
export * from './lib/http.js';
export * from './lib/http-app.js';
export * from './lib/model.js';
export * from './lib/logger.js';
export * from './lib/router.js';
Expand Down
11 changes: 11 additions & 0 deletions packages/http/src/lib/http-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { App, RootModuleDefinition } from '@deepkit/app';
import { HttpRouterRegistry } from './router.js';

/**
* Same as App, but with easily accessible router to make the most common use case easier.
*/
export class HttpApp<T extends RootModuleDefinition> extends App<T> {
get router(): HttpRouterRegistry {
return super.get(HttpRouterRegistry);
}
}
14 changes: 9 additions & 5 deletions packages/http/src/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,20 +591,24 @@ export class HttpListener {

async function next() {
i++;

if (i >= middlewares.length) {
event.response.off('finish', finish);
resolve(undefined);
return;
}

lastTimer = setTimeout(() => {
logger.warning(`Middleware timed out. Increase the timeout or fix the middleware. (${middlewares[i].fn})`);
next();
}, middlewares[i].timeout);
const timeout = middlewares[i].timeout;
if (timeout !== undefined && timeout > 0) {
lastTimer = setTimeout(() => {
logger.warning(`Middleware timed out. Increase the timeout or fix the middleware. (${middlewares[i].fn})`);
next();
}, timeout);
}

try {
await middlewares[i].fn(event.request, event.response, (error?: any) => {
clearTimeout(lastTimer);
if (lastTimer) clearTimeout(lastTimer);
if (error) {
event.response.off('finish', finish);
reject(error);
Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/lib/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class HttpMiddlewareConfig {
routeNames: string[] = [];
excludeRouteNames: string[] = [];

timeout: number = 4_000;
timeout?: number;

modules: InjectorModule<any>[] = [];

Expand Down
3 changes: 2 additions & 1 deletion packages/http/src/lib/request-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
ValidationError
} from '@deepkit/type';
import { BodyValidationError, createRequestWithCachedBody, getRegExp, HttpRequest, ValidatedBody } from './model.js';
import { RouteConfig, UploadedFile } from './router.js';
import { RouteConfig, UploadedFile, UploadedFileSymbol } from './router.js';

//@ts-ignore
import qs from 'qs';
Expand All @@ -40,6 +40,7 @@ function parseBody(
for (const [name, file] of Object.entries(files) as any) {
if (file.size === 0) continue;
foundFiles[name] = {
validator: UploadedFileSymbol,
size: file.size,
path: file.filepath,
name: file.originalFilename,
Expand Down
20 changes: 15 additions & 5 deletions packages/http/src/lib/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* You should have received a copy of the MIT License along with this program.
*/
import { ClassType, CompilerContext, getClassName, isArray, isClass, urlJoin } from '@deepkit/core';
import { entity, ReflectionClass, ReflectionFunction, ReflectionKind, ReflectionParameter, SerializationOptions, Serializer, Type, ValidationError } from '@deepkit/type';
import { entity, ReflectionClass, ReflectionFunction, ReflectionKind, ReflectionParameter, SerializationOptions, serializer, Serializer, Type, ValidationError } from '@deepkit/type';
import { HttpAction, httpClass, HttpController, HttpDecorator } from './decorator.js';
import { HttpRequest, HttpRequestPositionedParameters, HttpRequestQuery, HttpRequestResolvedParameters } from './model.js';
import { InjectorContext, InjectorModule, TagRegistry } from '@deepkit/injector';
Expand All @@ -29,11 +29,18 @@ interface ResolvedController {
parameters: RouteParameterResolverForInjector;
routeConfig: RouteConfig;
uploadedFiles: { [name: string]: UploadedFile };
middlewares?: (injector: InjectorContext) => { fn: HttpMiddlewareFn, timeout: number }[];
middlewares?: (injector: InjectorContext) => { fn: HttpMiddlewareFn, timeout?: number }[];
}

export const UploadedFileSymbol = Symbol('UploadedFile');

@entity.name('@deepkit/UploadedFile')
export class UploadedFile {
/**
* Validator to ensure the file was provided by the framework, and not the user spoofing it.
*/
validator!: typeof UploadedFileSymbol | null;

/**
* The size of the uploaded file in bytes.
*/
Expand Down Expand Up @@ -66,6 +73,11 @@ export class UploadedFile {
// hash!: string | 'sha1' | 'md5' | 'sha256' | null;
}

serializer.typeGuards.getRegistry(1).registerClass(UploadedFile, (type, state) => {
state.setContext({ UploadedFileSymbol });
state.addSetterAndReportErrorIfInvalid('uploadSecurity', 'Not an uploaded file', `typeof ${state.accessor} === 'object' && ${state.accessor} !== null && ${state.accessor}.validator === UploadedFileSymbol`);
});

export interface RouteFunctionControllerAction {
type: 'function';
//if not set, the root module is used
Expand Down Expand Up @@ -533,7 +545,6 @@ export class HttpRouter {
constructor(
controllers: HttpControllers,
private logger: LoggerInterface,
tagRegistry: TagRegistry,
private config: HttpConfig,
private middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry,
private registry: HttpRouterRegistry = new HttpRouterRegistry,
Expand All @@ -549,14 +560,13 @@ export class HttpRouter {

static forControllers(
controllers: (ClassType | { module: InjectorModule<any>, controller: ClassType })[],
tagRegistry: TagRegistry = new TagRegistry(),
middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry(),
module: InjectorModule<any> = new InjectorModule(),
config: HttpConfig = new HttpConfig()
): HttpRouter {
return new this(new HttpControllers(controllers.map(v => {
return isClass(v) ? { controller: v, module } : v;
})), new Logger([], []), tagRegistry, config, middlewareRegistry);
})), new Logger([], []), config, middlewareRegistry);
}

protected getRouteCode(compiler: CompilerContext, routeConfig: RouteConfig): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/http/tests/middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Controller {
test('middleware empty', async () => {
const httpKernel = createHttpKernel([Controller], [], [], [httpMiddleware.for((req, res, next) => {
next();
})]);
}).timeout(100)]);

const response = await httpKernel.request(HttpRequest.GET('/user/name1'));
expect(response.statusCode).toEqual(200);
Expand Down
49 changes: 49 additions & 0 deletions packages/http/tests/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1350,3 +1350,52 @@ test('fetch thrown error instances in listeners', async () => {

expect((await httpKernel.request(HttpRequest.GET('/'))).statusCode).toEqual(403);
});

test('upload security', async () => {
class UploadBody {
someFile!: UploadedFile;
}

class Controller {
@http.POST('/upload')
upload(body: HttpBody<UploadBody>): any {
return { uploadedSize: body.someFile.size };
}
}

const httpKernel = createHttpKernel([Controller]);

expect((await httpKernel.request(HttpRequest.POST('/upload').json({
someFile: {
size: 12345,
path: '/etc/secure-file',
name: 'fakefile',
type: 'image/jpeg',
lastModifiedDate: null
}
}))).json).toMatchObject({
message: 'Validation error:\nsomeFile(uploadSecurity): Not an uploaded file'
});

// ensure type deserialization doesn't set the invalid 'fake value' value to UploadedFileSymbol
expect((await httpKernel.request(HttpRequest.POST('/upload').json({
someFile: {
validator: 'fake value',
size: 12345,
path: '/etc/secure-file',
name: 'fakefile',
type: 'image/jpeg',
lastModifiedDate: null
}
}))).json).toMatchObject({
message: 'Validation error:\nsomeFile(uploadSecurity): Not an uploaded file'
});

expect((await httpKernel.request(HttpRequest.POST('/upload').multiPart([
{
name: 'someFile',
file: Buffer.from('testing a text file'),
fileName: 'test.txt'
}
]))).json).toMatchObject({ uploadedSize: 19 });
});
Loading

0 comments on commit 3575be3

Please sign in to comment.