From 30939492b96fd90d027728fc7a3030e8d8d24bf3 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Sun, 12 Jun 2022 18:11:49 +0200 Subject: [PATCH] feature(type-compiler): stop using TypeChecker entirely and allow loader-pattern, to better support webpack/esbuild/SWC. chore(type): moved tests files, since all will container reflection now. feature: started using .js file extension in imports to support esm. feature(rpc): added RpcWebSocketServer feature(http): support functional route definitions (alternative to classes). feature(event): support functional event listeners (alternative to classes). breaking(http): renamed Router to HttpRouter. breaking(rpc): renamed TcpRpcServer = > RpcTcpServer, TcpRpcClientAdapter => RpcTcpClientAdapter, NetTcpRpcClientAdapter => RpcNetTcpClientAdapter --- jest-resolver.js | 2 +- package-lock.json | 60 +- packages/angular-universal/src/listener.ts | 2 +- packages/api-console-module/src/controller.ts | 18 +- packages/app/src/app.ts | 18 +- packages/app/src/command.ts | 32 +- packages/app/src/module.ts | 8 +- packages/app/src/service-container.ts | 10 +- packages/app/tests/application.spec.ts | 70 +- .../benchmark/src/language/pass-args.bench.ts | 99 ++- packages/bson/tsconfig.json | 1 + packages/core/index.ts | 30 +- packages/core/package.json | 3 + packages/core/src/compiler.ts | 2 +- packages/core/src/core.ts | 4 +- packages/core/src/decorators.ts | 4 +- packages/core/src/emitter.ts | 2 +- packages/core/src/enum.ts | 2 +- packages/core/src/process-locker.ts | 2 +- packages/core/src/reflection.ts | 2 +- packages/event/package.json | 3 + packages/event/src/event.ts | 94 +-- packages/event/tests/basic.spec.ts | 14 + packages/event/tsconfig.json | 3 +- packages/example-app/app.ts | 13 +- packages/example-app/package.json | 1 + .../example-app/src/controller/users.cli.ts | 11 +- packages/framework-integration/tests/util.ts | 8 +- packages/framework/src/application-server.ts | 10 +- packages/framework/src/broker/broker.ts | 10 +- packages/framework/src/cli/debug-router.ts | 4 +- packages/framework/src/debug/broker.ts | 6 +- .../framework/src/debug/debug.controller.ts | 6 +- packages/framework/src/module.ts | 2 +- packages/framework/tests/http.spec.ts | 22 + .../framework/tests/service-container.spec.ts | 22 +- packages/http/package.json | 3 + packages/http/src/decorator.ts | 15 +- packages/http/src/filter.ts | 8 +- packages/http/src/http.ts | 44 +- packages/http/src/kernel.ts | 4 +- packages/http/src/model.ts | 31 +- packages/http/src/module.ts | 8 +- packages/http/src/router.ts | 320 +++++++-- packages/http/src/static-serving.ts | 9 +- packages/http/tests/filter.spec.ts | 12 +- packages/http/tests/middleware.spec.ts | 4 +- packages/http/tests/module.spec.ts | 41 ++ packages/http/tests/router-functional.spec.ts | 87 +++ packages/http/tests/router.spec.ts | 71 +- packages/http/tests/utils.ts | 39 +- packages/injector/index.ts | 8 +- packages/injector/package.json | 3 + packages/injector/src/injector.ts | 81 ++- packages/injector/src/module.ts | 14 +- packages/injector/src/provider.ts | 2 +- packages/injector/tests/injector.spec.ts | 82 ++- packages/injector/tests/injector2.spec.ts | 10 +- packages/mongo/package-lock.json | 2 +- packages/orm/package.json | 3 + packages/orm/src/database-adapter.ts | 2 +- packages/orm/src/database-session.ts | 24 +- packages/orm/src/database.ts | 19 +- packages/orm/src/event.ts | 8 +- packages/orm/src/query.ts | 2 +- packages/orm/src/utils.ts | 2 +- packages/rpc-tcp/package-lock.json | 58 +- packages/rpc-tcp/package.json | 4 +- packages/rpc-tcp/src/client.ts | 4 +- packages/rpc-tcp/src/server.ts | 59 +- packages/rpc/src/client/client-websocket.ts | 2 +- packages/rpc/src/client/client.ts | 8 +- packages/rpc/src/client/message-subject.ts | 8 +- packages/rpc/src/protocol.ts | 4 +- packages/rpc/src/server/action.ts | 2 +- packages/rpc/src/server/kernel.ts | 14 +- packages/rpc/tests/back-controller.spec.ts | 4 +- packages/rpc/tests/case/crud.spec.ts | 2 +- packages/rpc/tests/chunks.spec.ts | 2 +- packages/rpc/tests/collection.spec.ts | 2 +- packages/rpc/tests/controller.spec.ts | 21 +- packages/rpc/tests/entity-state.spec.ts | 2 +- packages/rpc/tests/observable.spec.ts | 18 +- packages/rpc/tests/rpc.spec.ts | 4 +- packages/rpc/tests/security.spec.ts | 8 +- packages/sql/src/cli/base-command.ts | 2 +- packages/sql/src/sql-adapter.ts | 2 +- packages/sqlite/src/sqlite-adapter.ts | 3 + packages/template/src/template.ts | 18 +- packages/type-compiler/index.ts | 1 + packages/type-compiler/install-transformer.ts | 1 - packages/type-compiler/package.json | 12 +- packages/type-compiler/src/compiler.ts | 654 +++++++++++++----- packages/type-compiler/src/loader.ts | 59 ++ packages/type-compiler/src/reflection-ast.ts | 1 + packages/type-compiler/src/resolver.ts | 58 ++ packages/type-compiler/src/ts-types.ts | 54 +- .../type-compiler/tests/compiler-vfs.spec.ts | 73 -- .../type-compiler/tests/transform.spec.ts | 112 +++ .../type-compiler/tests/transpile.spec.ts | 255 +++++++ packages/type-compiler/tests/utils.ts | 118 ++++ packages/type-compiler/tsconfig.json | 1 - packages/type-spec/index.ts | 2 +- packages/type/index.ts | 48 +- packages/type/package-lock.json | 281 +++++++- packages/type/package.json | 8 +- packages/type/src/change-detector.ts | 14 +- packages/type/src/changes.ts | 8 +- packages/type/src/decorator-builder.ts | 55 +- packages/type/src/decorator.ts | 10 +- packages/type/src/default.ts | 2 +- packages/type/src/inheritance.ts | 4 +- packages/type/src/mixin.ts | 2 +- packages/type/src/path.ts | 6 +- packages/type/src/reference.ts | 8 +- packages/type/src/reflection/extends.ts | 5 +- packages/type/src/reflection/processor.ts | 31 +- packages/type/src/reflection/reflection.ts | 128 ++-- packages/type/src/reflection/type.ts | 44 +- packages/type/src/registry.ts | 2 +- packages/type/src/serializer-facade.ts | 10 +- packages/type/src/serializer.ts | 18 +- packages/type/src/snapshot.ts | 8 +- packages/type/src/type-serialization.ts | 8 +- packages/type/src/typeguard.ts | 10 +- packages/type/src/types.ts | 4 +- packages/type/src/validator.ts | 10 +- packages/type/src/validators.ts | 4 +- packages/type/tests/compiler.spec.ts | 181 +++-- packages/type/tests/complex-query.spec.ts | 408 +++++++++++ packages/type/tests/integration.spec.ts | 121 ++-- packages/type/tests/simple-decorator.spec.ts | 19 + packages/type/tests/type.spec.ts | 83 ++- packages/type/tests/validation.spec.ts | 30 +- packages/workflow/src/workflow.ts | 24 +- packages/workflow/tests/workflow.spec.ts | 10 +- 136 files changed, 3737 insertions(+), 1077 deletions(-) create mode 100644 packages/event/tests/basic.spec.ts create mode 100644 packages/framework/tests/http.spec.ts create mode 100644 packages/http/tests/router-functional.spec.ts create mode 100644 packages/type-compiler/src/loader.ts create mode 100644 packages/type-compiler/src/resolver.ts delete mode 100644 packages/type-compiler/tests/compiler-vfs.spec.ts create mode 100644 packages/type-compiler/tests/transform.spec.ts create mode 100644 packages/type-compiler/tests/transpile.spec.ts create mode 100644 packages/type-compiler/tests/utils.ts create mode 100644 packages/type/tests/complex-query.spec.ts create mode 100644 packages/type/tests/simple-decorator.spec.ts diff --git a/jest-resolver.js b/jest-resolver.js index c1cf5786e..e6b97bf95 100644 --- a/jest-resolver.js +++ b/jest-resolver.js @@ -1,5 +1,5 @@ const resolve = require('enhanced-resolve'); -const resolve2 = require('resolve'); +// const resolve2 = require('resolve'); /** * This custom jest resolver makes sure symlinks are not followed, so preserveSymlinks=true. diff --git a/package-lock.json b/package-lock.json index 53e3536a0..d14aaf865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2965,6 +2965,21 @@ "node": ">=10" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.0.tgz", @@ -11951,21 +11966,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/tsutils": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", - "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -12115,9 +12115,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15077,6 +15077,15 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } } } }, @@ -21984,15 +21993,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "tsutils": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", - "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -22103,9 +22103,9 @@ "dev": true }, "typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true }, "uglify-js": { diff --git a/packages/angular-universal/src/listener.ts b/packages/angular-universal/src/listener.ts index 3e40c6801..b8fd0532e 100644 --- a/packages/angular-universal/src/listener.ts +++ b/packages/angular-universal/src/listener.ts @@ -117,7 +117,7 @@ export class AngularUniversalListener { } event.routeFound( - new RouteConfig('angular', ['GET'], event.url, { controller: AngularUniversalListener, module: this.module, methodName: 'render' }), + new RouteConfig('angular', ['GET'], event.url, { type: 'controller', controller: AngularUniversalListener, module: this.module, methodName: 'render' }), () => [event.url] ); } diff --git a/packages/api-console-module/src/controller.ts b/packages/api-console-module/src/controller.ts index f3697db6f..3a3866733 100644 --- a/packages/api-console-module/src/controller.ts +++ b/packages/api-console-module/src/controller.ts @@ -1,19 +1,19 @@ import { ApiAction, ApiConsoleApi, ApiDocument, ApiEntryPoints, ApiRoute, ApiRouteResponse } from '@deepkit/api-console-api'; import { getActions, rpc, RpcKernel } from '@deepkit/rpc'; import { HttpRouteFilter, HttpRouterFilterResolver, parseRouteControllerAction } from '@deepkit/http'; -import { ClassType, getClassName } from '@deepkit/core'; +import { ClassType, getClassName, isClass } from '@deepkit/core'; import { Config } from './module.config'; import { readFile } from 'fs/promises'; import { ReflectionClass, ReflectionKind, serializeType, Type, TypeClass, TypeObjectLiteral, TypePropertySignature } from '@deepkit/type'; class ControllerNameGenerator { - controllers = new Map(); + controllers = new Map(); controllerNames = new Set(); - getName(controller: ClassType): string { + getName(controller: ClassType | Function): string { let controllerName = this.controllers.get(controller); if (!controllerName) { - controllerName = getClassName(controller); + controllerName = isClass(controller) ? getClassName(controller) : controller.name; let candidate = controllerName; let i = 2; while (this.controllerNames.has(candidate)) { @@ -83,7 +83,7 @@ export class ApiConsoleController implements ApiConsoleApi { try { //todo: Collection, SubjectEntity, Observable get pretty big - rpcAction.methodType = serializeType(reflectionMethod.method); + rpcAction.methodType = serializeType(reflectionMethod.type); } catch (error: any) { console.log(`Could not serialize result type of ${of}: ${error.message}`); } @@ -103,12 +103,12 @@ export class ApiConsoleController implements ApiConsoleApi { for (const route of this.filterResolver.resolve(this.filter.model)) { if (route.internal) continue; - const controllerName = nameGenerator.getName(route.action.controller); + const controllerName = nameGenerator.getName( route.action.type === 'controller' ? route.action.controller : route.action.fn); const routeD = new ApiRoute( route.getFullPath(), route.httpMethods, controllerName, - route.action.methodName, + route.action.type === 'controller' ? route.action.methodName : '', route.description, route.groups, route.category, @@ -167,9 +167,9 @@ export class ApiConsoleController implements ApiConsoleApi { } } - const reflectionMethod = ReflectionClass.from(route.action.controller).getMethod(route.action.methodName); + const fn = route.getReflectionFunction(); + routeD.resultType = serializeType(fn.getReturnType()); - routeD.resultType = serializeType(reflectionMethod.getReturnType()); if (urlType.types.length) routeD.urlType = serializeType(urlType); if (queryType.types.length) routeD.queryType = serializeType(queryType); routes.push(routeD); diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts index bb6f07e3d..322ec89e0 100644 --- a/packages/app/src/app.ts +++ b/packages/app/src/app.ts @@ -19,6 +19,7 @@ import { ExitError } from '@oclif/errors'; import { buildOclifCommand } from './command'; import { EnvConfiguration } from './configuration'; import { ReflectionClass, ReflectionKind } from '@deepkit/type'; +import { EventDispatcher, EventListener, EventListenerCallback, EventOfEventToken, EventToken } from '@deepkit/event'; export function setPartialConfig(target: { [name: string]: any }, partial: { [name: string]: any }, incomingPath: string = '') { for (const i in partial) { @@ -148,7 +149,7 @@ class EnvConfigLoader { } } -export class RootAppModule extends AppModule { +export class RootAppModule extends AppModule { } /** @@ -176,10 +177,13 @@ export class App { this.serviceContainer = serviceContainer || new ServiceContainer(this.appModule); } - static fromModule(module: AppModule): App { + static fromModule(module: AppModule): App { return new App({} as T, undefined, module); } + /** + * Allows to change the module after the configuration has been loaded, right before the application bootstraps. + */ setup(...args: Parameters): this { this.serviceContainer.appModule = (this.serviceContainer.appModule.setup as any)(...args as any[]); return this; @@ -195,6 +199,16 @@ export class App { return this; } + listen, DEPS extends any[]>(eventToken: T, callback: EventListenerCallback, order: number = 0): this { + const listener: EventListener = { callback, order, eventToken }; + this.appModule.listeners.push(listener); + return this; + } + + public async dispatch>(eventToken: T, event: EventOfEventToken, injector?: InjectorContext): Promise { + return await this.get(EventDispatcher).dispatch(eventToken, event, injector); + } + /** * Loads environment variables and optionally reads from .env files in order to find matching configuration options * in your application and modules in order to set their values. diff --git a/packages/app/src/command.ts b/packages/app/src/command.ts index 49f99f2e8..0ae3501ac 100644 --- a/packages/app/src/command.ts +++ b/packages/app/src/command.ts @@ -21,7 +21,7 @@ import { ReflectionParameter, ReflectionProperty, validate, - ValidationErrorItem + ValidationError } from '@deepkit/type'; import { Command as OclifCommandBase } from '@oclif/command'; import { Command as OclifCommand } from '@oclif/config'; @@ -61,7 +61,7 @@ function getProperty(classType: ClassType, ref: {property: string; parameterInde class ArgDefinition { isFlag: boolean = false; - multiple: boolean = false; + description: string = ''; hidden: boolean = false; char: string = ''; property!: string; @@ -88,16 +88,15 @@ export class ArgDecorator implements PropertyApiTypeInterface { cli.addArg(this.t)(classType); } - get multiple() { - this.t.multiple = true; - return; - } - get hidden() { this.t.hidden = true; return; } + description(description: string) { + this.t.description = description; + } + char(char: string) { this.t.char = char; } @@ -154,16 +153,19 @@ export function buildOclifCommand(name: string, injector: InjectorContext, class const options = { name: propertySchema.name, - description: 'todo', + description: t.description, hidden: t.hidden, required: !(propertySchema.isOptional() || propertySchema.hasDefault()), - multiple: t.multiple, + multiple: propertySchema.getType().kind === ReflectionKind.array, default: propertySchema.getDefaultValue(), }; //todo, add `parse(i)` and make sure type is correct depending on t.propertySchema.type if (t.isFlag) { oclifFlags[propertySchema.name] = propertySchema.type.kind === ReflectionKind.boolean ? flags.boolean(options) : flags.string(options); + if (t.char) { + oclifFlags[propertySchema.name].char = t.char as any; + } } else { oclifArgs.push(options); } @@ -188,9 +190,9 @@ export function buildOclifCommand(name: string, injector: InjectorContext, class const methodArgs: any[] = []; for (const property of argDefinitions!.args) { - try { - const propertySchema = getProperty(classType, property); + const propertySchema = getProperty(classType, property); + try { const v = converters.get(propertySchema)!(args[propertySchema.name] ?? flags[propertySchema.name]); if (propertySchema instanceof ReflectionParameter) { methodArgs.push(v); @@ -200,11 +202,13 @@ export function buildOclifCommand(name: string, injector: InjectorContext, class } } } catch (e) { - if (e instanceof ValidationErrorItem) { - console.log(`Validation error in ${e.path}: ${e.message} [${e.code}]`); + if (e instanceof ValidationError) { + for (const item of e.errors) { + console.error(`Validation error in ${propertySchema.name + (item.path ? '.' + item.path : '')}: ${item.message} [${item.code}]`); + } return 8; } - console.log(e); + console.error(e); return 8; } } diff --git a/packages/app/src/module.ts b/packages/app/src/module.ts index d8fbeec43..ec8eb82f2 100644 --- a/packages/app/src/module.ts +++ b/packages/app/src/module.ts @@ -36,10 +36,10 @@ export interface ModuleDefinition { exports?: ExportType[]; /** - * Module bootstrap class. This class is instantiated on bootstrap and can - * setup various injected services. A more flexible alternative is to use .setup() with compiler passes. + * Module bootstrap class|function. + * This class is instantiated or function executed on bootstrap and can set up various injected services. */ - bootstrap?: ClassType; + bootstrap?: ClassType | Function; /** * Configuration definition. @@ -257,7 +257,7 @@ export class AppModule ): void { - if (module.injector) throw new Error(`Module ${getClassName(module)}.${module.name} was already imported. Can not re-use module instances.`); + if (module.injector) { + throw new Error(`Module ${getClassName(module)} (id=${module.name}) was already imported. Can not re-use module instances.`); + } const providers = module.getProviders(); const controllers = module.getControllers(); const listeners = module.getListeners(); const middlewares = module.getMiddlewares(); - if (module.options.bootstrap && !module.isProvided(module.options.bootstrap)) { + if (module.options.bootstrap && !isFunction(module.options.bootstrap) && !module.isProvided(module.options.bootstrap)) { providers.push(module.options.bootstrap); } @@ -224,7 +226,7 @@ export class ServiceContainer { providers.unshift({ provide: listener }); this.eventDispatcher.registerListener(listener, module); } else { - this.eventDispatcher.add(listener.eventToken, { fn: listener.callback, order: listener.order, module: listener.module }); + this.eventDispatcher.add(listener.eventToken, { fn: listener.callback, order: listener.order, module: listener.module || module }); } } diff --git a/packages/app/tests/application.spec.ts b/packages/app/tests/application.spec.ts index ac284fb1e..fac820b4a 100644 --- a/packages/app/tests/application.spec.ts +++ b/packages/app/tests/application.spec.ts @@ -2,17 +2,19 @@ import { beforeEach, expect, test } from '@jest/globals'; import { App } from '../src/app'; import { Inject, ProviderWithScope, Token } from '@deepkit/injector'; import { AppModule, createModule } from '../src/module'; -import { BaseEvent, EventDispatcher, eventDispatcher, EventToken } from '@deepkit/event'; +import { BaseEvent, EventDispatcher, eventDispatcher, EventToken, DataEventToken } from '@deepkit/event'; import { cli, Command } from '../src/command'; import { ClassType } from '../../core'; import { isClass } from '@deepkit/core'; import { ServiceContainer } from '../src/service-container'; +import { DataEvent } from '@deepkit/event'; Error.stackTraceLimit = 100; class BaseConfig { db: string = 'notSet'; } + class BaseService { constructor(public db: BaseConfig['db']) { } @@ -211,7 +213,7 @@ test('loadConfigFromEnvVariables() happens before setup() calls', async () => { test('config uppercase naming strategy', async () => { class Config { - dbHost!: string + dbHost!: string; } const app = new App({ config: Config }).setup((module, config) => { @@ -274,6 +276,41 @@ test('non-forRoot module with class listeners works without exports', async () = expect(executed).toBe(true); }); +test('listen() with dependencies', async () => { + interface EventData { + id: number; + } + + const myEvent = new DataEventToken('my-event'); + + class MyService { + } + + class MyConfig { + environment: string = 'dev'; + } + const gotEvents: number[] = []; + const myModule = new AppModule({ + config: MyConfig, + providers: [MyService], + listeners: [ + myEvent.listen((event, service: MyService, env: MyConfig['environment']) => { + if (!(service instanceof MyService)) throw new Error('Got no service'); + expect(env).toBe('dev'); + gotEvents.push(event.data.id); + }) + ] + }, 'base'); + + const app = new App({ imports: [myModule] }); + const dispatcher = app.get(EventDispatcher); + + await dispatcher.dispatch(myEvent, new DataEvent({ id: 2 })); + await dispatcher.dispatch(myEvent, new DataEvent({ id: 3 })); + + expect(gotEvents).toEqual([2, 3]); +}); + test('non-forRoot module with fn listeners works without exports', async () => { const myEvent = new EventToken('my-event'); @@ -482,3 +519,32 @@ test('App.get generic', () => { const service = app.get('service' as any); service.add(); }); + +test('event dispatch', () => { + interface User { + username: string; + } + + + class Logger { + buffer: string[][] = []; + + log(...message: string[]) { + console.log(...message); + this.buffer.push(message); + } + } + + const app = new App({ + providers: [Logger] + }); + + const UserAddedEvent = new DataEventToken('user-added'); + + app.listen(UserAddedEvent, (event, logger: Logger) => { + logger.log('User added', event.data.username); + }); + app.dispatch(UserAddedEvent, { username: 'Peter' }); + + expect(app.get(Logger).buffer).toEqual([['User added', 'Peter']]); +}); diff --git a/packages/benchmark/src/language/pass-args.bench.ts b/packages/benchmark/src/language/pass-args.bench.ts index b9f9e480a..ac584108f 100644 --- a/packages/benchmark/src/language/pass-args.bench.ts +++ b/packages/benchmark/src/language/pass-args.bench.ts @@ -2,41 +2,110 @@ import { BenchSuite } from '../bench'; export function main() { const bench = new BenchSuite('map'); + const g = globalThis; + g.Ω = []; + + globalThis.Ω = []; function a(type) { } - function b(type = (b as any).Ω) { + function b(type = globalThis.Ω[0]) { } - class Database { - a(type) { + // class Database { + // a(type) { + // } + // b(type: any = (this as any).b.Ω) { + // } + // } + + bench.add('function direct', () => { + b(); + }); + + // bench.add('function with types', () => { + // ((b as any).Ω = () => [], b)(); + // }); + // + // bench.add('function indirect', () => { + // //@ts-ignore + // ((b as any).Ω = [], b)(); + // }); + + class Decorator { + protected yes = true; + + response() { + if (!this.yes) throw new Error('Lost context'); + return this; } - b(type: any = (this as any).b.Ω) { + + response2() { + if (!this.yes) throw new Error('Lost context'); + return this; } } - bench.add('function direct', () => { - b(); + const d = new Decorator(); + + bench.add('deep direct', () => { + d.response().response2(); }); - bench.add('function with types', () => { - ((b as any).Ω = () => [], b)(); + var r; + + bench.add('deep pass', () => { + (((globalThis as any).Ω = [], r = d.response()), (globalThis as any).Ω = [], r).response2(); }); - bench.add('function indirect', () => { + bench.add('function indirect 2', () => { //@ts-ignore - ((b as any).Ω = [], b)(); + b.Ω = []; + b(); + }); + + bench.add('function global', () => { + (globalThis.Ω = [], b)(); + }); + + function passArg(fn: Function, args: []): Function { + g.Ω = args; + return fn; + } + + bench.add('pass arg', () => { + passArg(() => {}, [])(); }); - const db = new Database(); - bench.add('method direct', () => { - db.b(); + bench.add('dynamic function', () => { + (() => { + })(); }); - bench.add('method with types', () => { - ((db.b as any).Ω = () => [], db).b(); + bench.add('dynamic function object.assign __type assignment', () => { + Object.assign(() => { + }, { __type: [] })(); }); + function assignType(fn: Function, type: any): Function { + if (!(fn as any).__type) (fn as any).__type = type; + return fn; + } + + bench.add('dynamic function custom __type assignment', () => { + assignType(() => { + }, [])(); + }); + + // const db = new Database(); + // bench.add('method direct', () => { + // db.b(); + // }); + + // bench.add('method with types', () => { + // ((db.b as any).Ω = () => [], db).b(); + // }); + bench.run(); } diff --git a/packages/bson/tsconfig.json b/packages/bson/tsconfig.json index ab986a3cc..61cf5b9d3 100644 --- a/packages/bson/tsconfig.json +++ b/packages/bson/tsconfig.json @@ -16,6 +16,7 @@ "declaration": true, "composite": true }, + "reflection": true, "include": [ "benchmarks", "src", diff --git a/packages/core/index.ts b/packages/core/index.ts index b3e27d01c..0ad67cee3 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -8,18 +8,18 @@ * You should have received a copy of the MIT License along with this program. */ -export * from './src/core'; -export * from './src/decorators'; -export * from './src/enum'; -export * from './src/iterators'; -export * from './src/timer'; -export * from './src/process-locker'; -export * from './src/network'; -export * from './src/perf'; -export * from './src/compiler'; -export * from './src/string'; -export * from './src/emitter'; -export * from './src/reactive'; -export * from './src/reflection'; -export * from './src/url'; -export * from './src/array'; +export * from './src/core.js'; +export * from './src/decorators.js'; +export * from './src/enum.js'; +export * from './src/iterators.js'; +export * from './src/timer.js'; +export * from './src/process-locker.js'; +export * from './src/network.js'; +export * from './src/perf.js'; +export * from './src/compiler.js'; +export * from './src/string.js'; +export * from './src/emitter.js'; +export * from './src/reactive.js'; +export * from './src/reflection.js'; +export * from './src/url.js'; +export * from './src/array.js'; diff --git a/packages/core/package.json b/packages/core/package.json index 7025689a9..2404f4f0f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,6 +33,9 @@ "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "testMatch": [ "**/tests/**/*.spec.ts" ] diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts index dd75a00c3..a619dba02 100644 --- a/packages/core/src/compiler.ts +++ b/packages/core/src/compiler.ts @@ -8,7 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ // @ts-ignore -import { indent } from './indent'; +import { indent } from './indent.js'; export class CompilerContext { public readonly context = new Map(); diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 4d82ff243..e71dcba7c 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -9,7 +9,7 @@ */ import dotProp from 'dot-prop'; -import { eachPair } from './iterators'; +import { eachPair } from './iterators.js'; /** * Makes sure the error once printed using console.log contains the actual class name. @@ -66,7 +66,7 @@ export type ExtractClassType = T extends AbstractClassType ? K : nev export function getClassName(classTypeOrInstance: ClassType | Object): string { if (!classTypeOrInstance) return 'undefined'; const proto = (classTypeOrInstance as any)['prototype'] ? (classTypeOrInstance as any)['prototype'] : classTypeOrInstance; - return proto.constructor.name || 'AnonymousClass'; + return proto.constructor.name || 'anonymous class'; } /** diff --git a/packages/core/src/decorators.ts b/packages/core/src/decorators.ts index 99a4f1010..00738ddde 100644 --- a/packages/core/src/decorators.ts +++ b/packages/core/src/decorators.ts @@ -8,8 +8,8 @@ * You should have received a copy of the MIT License along with this program. */ -import { getClassName } from './core'; -import { toFastProperties } from './perf'; +import { getClassName } from './core.js'; +import { toFastProperties } from './perf.js'; /** * Logs every call to this method on stdout. diff --git a/packages/core/src/emitter.ts b/packages/core/src/emitter.ts index 4fe85c4f4..e545cec82 100644 --- a/packages/core/src/emitter.ts +++ b/packages/core/src/emitter.ts @@ -8,7 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { arrayRemoveItem } from './array'; +import { arrayRemoveItem } from './array.js'; type AsyncSubscriber = (event: T) => Promise | void; diff --git a/packages/core/src/enum.ts b/packages/core/src/enum.ts index fab850458..2fa778e74 100644 --- a/packages/core/src/enum.ts +++ b/packages/core/src/enum.ts @@ -8,7 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { eachKey } from './iterators'; +import { eachKey } from './iterators.js'; const cacheEnumLabels = new Map(); diff --git a/packages/core/src/process-locker.ts b/packages/core/src/process-locker.ts index 2a1d02231..07a8e770e 100644 --- a/packages/core/src/process-locker.ts +++ b/packages/core/src/process-locker.ts @@ -8,7 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { arrayRemoveItem } from './array'; +import { arrayRemoveItem } from './array.js'; const LOCKS: { [id: string]: { time: number, queue: Function[] } } = {}; diff --git a/packages/core/src/reflection.ts b/packages/core/src/reflection.ts index cd32f2d0c..2bd0f570e 100644 --- a/packages/core/src/reflection.ts +++ b/packages/core/src/reflection.ts @@ -7,7 +7,7 @@ * * You should have received a copy of the MIT License along with this program. */ -import { ClassType } from "./core"; +import { ClassType } from "./core.js"; const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; const DEFAULT_PARAMS = /=[^,]+/mg; diff --git a/packages/event/package.json b/packages/event/package.json index d1b1dd5e7..1568e152a 100644 --- a/packages/event/package.json +++ b/packages/event/package.json @@ -35,6 +35,9 @@ "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "testMatch": [ "**/tests/**/*.spec.ts" ] diff --git a/packages/event/src/event.ts b/packages/event/src/event.ts index 582d871b4..7e7147043 100644 --- a/packages/event/src/event.ts +++ b/packages/event/src/event.ts @@ -9,10 +9,11 @@ */ import { ClassType, CompilerContext, isClass, isFunction } from '@deepkit/core'; +import { injectedFunction } from '@deepkit/injector'; import { InjectorContext, InjectorModule } from '@deepkit/injector'; import { ClassDecoratorResult, createClassDecoratorContext, createPropertyDecoratorContext, PropertyDecoratorResult } from '@deepkit/type'; -export type EventListenerCallback = (event: T) => void | Promise; +export type EventListenerCallback = (event: T, ...args: any[]) => void | Promise; export interface EventListener { eventToken: EventToken; @@ -21,7 +22,11 @@ export interface EventListener { order: number; } -export type EventOfEventToken = T extends EventToken ? E : BaseEvent; +export type EventOfEventToken = T extends EventToken ? E extends DataEvent ? D | E : E : BaseEvent; + +interface SimpleDataEvent extends BaseEvent { + data: T; +} export class EventToken { /** @@ -36,11 +41,15 @@ export class EventToken { ) { } - listen(callback: (event: T) => void, order: number = 0, module?: InjectorModule): EventListener { + listen(callback: (event: T, ...args: any[]) => void, order: number = 0, module?: InjectorModule): EventListener { return { eventToken: this, callback, order: order, module }; } } +export class DataEventToken extends EventToken> { + +} + export class BaseEvent { stopped = false; @@ -115,7 +124,7 @@ export function isEventListenerContainerEntryService(obj: any): obj is EventList } interface EventDispatcherFn { - (instances: any[], scopedContext: InjectorContext, eventToken: EventToken, event: BaseEvent): Promise; + (scopedContext: InjectorContext, event: BaseEvent): Promise; } export class EventDispatcher { @@ -123,8 +132,10 @@ export class EventDispatcher { protected instances: any[] = []; protected registeredClassTypes = new Set(); + protected symbol = Symbol('eventDispatcher'); + constructor( - public scopedContext: InjectorContext = InjectorContext.forProviders([]), + public injector: InjectorContext = InjectorContext.forProviders([]), ) { } @@ -138,13 +149,12 @@ export class EventDispatcher { } } - public registerCallback(eventToken: EventToken, callback: (event: E) => Promise | void, order: number = 0) { + listen, DEPS extends any[]>(eventToken: T, callback: EventListenerCallback, order: number = 0): void { this.add(eventToken, { fn: callback, order: order }); } public add(eventToken: EventToken, listener: EventListenerContainerEntry) { this.getListeners(eventToken).push(listener); - (eventToken as any)[this.symbol] = this.buildFor(eventToken); } public getTokens(): EventToken[] { @@ -181,9 +191,11 @@ export class EventDispatcher { for (const listener of listeners) { if (isEventListenerContainerEntryCallback(listener)) { - const fnVar = compiler.reserveVariable('fn', listener.fn); + const injector = listener.module ? this.injector.getInjector(listener.module) : this.injector.getRootInjector(); + const fn = injectedFunction(listener.fn, injector, 1); + const fnVar = compiler.reserveVariable('fn', fn); lines.push(` - await ${fnVar}(event); + await ${fnVar}(scopedContext.scope, event); if (event.isStopped()) return; `); } else if (isEventListenerContainerEntryService(listener)) { @@ -208,21 +220,36 @@ export class EventDispatcher { switch (eventToken) { ${code.join('\n')} } - `, 'instances', 'scopedContext', 'eventToken', 'event') as EventDispatcherFn; + `, 'scopedContext', 'event') as EventDispatcherFn; } - protected symbol = Symbol('eventDispatcher'); - - protected buildFor(eventToken: EventToken) { + protected buildFor(eventToken: EventToken): EventDispatcherFn | undefined { const compiler = new CompilerContext(); const lines: string[] = []; - for (const listener of this.listenerMap.get(eventToken) || []) { + + const listeners = this.listenerMap.get(eventToken) || []; + if (!listeners.length) return; + + listeners.sort((a, b) => { + if (a.order > b.order) return +1; + if (a.order < b.order) return -1; + return 0; + }); + + for (const listener of listeners) { if (isEventListenerContainerEntryCallback(listener)) { - const fnVar = compiler.reserveVariable('fn', listener.fn); - lines.push(` - await ${fnVar}(event); - if (event.isStopped()) return; - `); + const injector = listener.module ? this.injector.getInjector(listener.module) : this.injector.getRootInjector(); + try { + const fn = injectedFunction(listener.fn, injector, 1); + const fnVar = compiler.reserveVariable('fn', fn); + lines.push(` + await ${fnVar}(scopedContext.scope, event); + if (event.isStopped()) return; + `); + } catch (error: any) { + throw error; + // throw new Error(`Could not build listener ${listener.fn.name || 'anonymous function'} of event token ${eventToken.id}: ${error.message}`); + } } else if (isEventListenerContainerEntryService(listener)) { const classTypeVar = compiler.reserveVariable('classType', listener.classType); const moduleVar = compiler.reserveVariable('module', listener.module); @@ -231,28 +258,17 @@ export class EventDispatcher { `); } } - return compiler.buildAsync(lines.join('\n'), 'scopedContext', 'event'); - } - - public dispatch>(eventToken: T, event: EventOfEventToken): Promise { - let fn = (eventToken as any)[this.symbol]; - //no fn means for this token does no listener exist - return fn ? fn(this.scopedContext, event) : undefined; + return compiler.buildAsync(lines.join('\n'), 'scopedContext', 'event') as EventDispatcherFn; } -} - -export type ExtractDep = T extends ClassType ? InstanceType : T; -export type ExtractDeps = { [K in keyof T]: ExtractDep } - -export function createListener, DEPS extends any[]>(eventToken: T, callback: (event: T['event'], ...deps: ExtractDeps) => void | Promise, ...deps: DEPS): ClassType { - class DynamicListener { - @eventDispatcher.listen(eventToken) - execute(event: T['event']) { - //todo: resolve deps - // return callback(event); + public async dispatch>(eventToken: T, event?: EventOfEventToken, injector?: InjectorContext): Promise { + let build = (eventToken as any)[this.symbol]; + if (!build) { + build = (eventToken as any)[this.symbol]= { fn: this.buildFor(eventToken) }; } - } - return DynamicListener; + //no fn means for this token has no listeners + const e = eventToken instanceof DataEventToken && (event as any) instanceof DataEvent ? event : new DataEvent(event); + return build.fn ? build.fn(injector || this.injector, e) : undefined; + } } diff --git a/packages/event/tests/basic.spec.ts b/packages/event/tests/basic.spec.ts new file mode 100644 index 000000000..04deb66f8 --- /dev/null +++ b/packages/event/tests/basic.spec.ts @@ -0,0 +1,14 @@ +import { test } from '@jest/globals'; +import { EventDispatcher, EventToken } from '../src/event.js'; + +test('functional api', async () => { + const dispatcher = new EventDispatcher(); + + const MyEvent = new EventToken('my-event'); + + dispatcher.listen(MyEvent, (event) => { + console.log('MyEvent triggered!'); + }); + + await dispatcher.dispatch(MyEvent); +}); diff --git a/packages/event/tsconfig.json b/packages/event/tsconfig.json index 85a51ad14..200d9bb5d 100644 --- a/packages/event/tsconfig.json +++ b/packages/event/tsconfig.json @@ -20,6 +20,7 @@ "tests", "index.ts" ], + "reflection": true, "references": [ { "path": "../core/tsconfig.json" @@ -31,4 +32,4 @@ "path": "../type/tsconfig.json" } ] -} \ No newline at end of file +} diff --git a/packages/example-app/app.ts b/packages/example-app/app.ts index dcae6df7c..e2c794e80 100755 --- a/packages/example-app/app.ts +++ b/packages/example-app/app.ts @@ -1,10 +1,10 @@ #!/usr/bin/env ts-node-script -import { createCrudRoutes, FrameworkModule } from '@deepkit/framework'; +import { createCrudRoutes, FrameworkModule, onServerMainBootstrapDone } from '@deepkit/framework'; import { Author, Book, SQLiteDatabase, User } from './src/database'; import { MainController } from './src/controller/main.http'; import { UsersCommand } from './src/controller/users.cli'; import { Config } from './src/config'; -import { JSONTransport, Logger } from '@deepkit/logger'; +import { JSONTransport, Logger, LoggerInterface } from '@deepkit/logger'; import { App } from '@deepkit/app'; import { RpcController } from './src/controller/rpc.controller'; import { ApiConsoleModule } from '@deepkit/api-console-module'; @@ -16,10 +16,9 @@ new App({ providers: [SQLiteDatabase, MainController], controllers: [MainController, UsersCommand, RpcController], listeners: [ - //todo: make that possible again - // createListener(onServerMainBootstrapDone, (event, logger, environment) => { - // logger.log(`Environment ${environment}`); - // }, Logger, config.token('environment')), + onServerMainBootstrapDone.listen((event, logger: LoggerInterface, environment: Config['environment']) => { + logger.log(`Environment ${environment}`); + }) ], imports: [ createCrudRoutes([User], { identifier: 'username', identifierChangeable: true }), @@ -50,7 +49,7 @@ new App({ if (config.environment === 'production') { //enable logging JSON messages instead of formatted strings - module.setupProvider().setTransport([new JSONTransport]); + module.setupGlobalProvider().setTransport([new JSONTransport]); } }) .loadConfigFromEnv() diff --git a/packages/example-app/package.json b/packages/example-app/package.json index 628338e59..35241b626 100644 --- a/packages/example-app/package.json +++ b/packages/example-app/package.json @@ -5,6 +5,7 @@ "description": "", "scripts": { "start": "NODE_OPTIONS=--preserve-symlinks ts-node app server:start", + "start:prod": "NODE_OPTIONS=--preserve-symlinks APP_ENVIRONMENT=production ts-node app server:start", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { diff --git a/packages/example-app/src/controller/users.cli.ts b/packages/example-app/src/controller/users.cli.ts index 7968b1b37..57bfc31a7 100644 --- a/packages/example-app/src/controller/users.cli.ts +++ b/packages/example-app/src/controller/users.cli.ts @@ -1,15 +1,16 @@ -import { cli, Command } from '@deepkit/app'; +import { arg, cli, Command, flag } from '@deepkit/app'; import { LoggerInterface } from '@deepkit/logger'; import { SQLiteDatabase, User } from '../database'; +import { Positive } from '@deepkit/type'; @cli.controller('users') export class UsersCommand implements Command { constructor(protected logger: LoggerInterface, protected database: SQLiteDatabase) { } - async execute(): Promise { - this.logger.log('Loading users ...'); - const users = await this.database.query(User).find(); - console.table(users); + async execute(@flag id: number[] = []): Promise { + this.logger.log('Loading users ...', id); + // const users = await this.database.query(User).find(); + // console.table(users); } } diff --git a/packages/framework-integration/tests/util.ts b/packages/framework-integration/tests/util.ts index 82e34061f..ece27df73 100644 --- a/packages/framework-integration/tests/util.ts +++ b/packages/framework-integration/tests/util.ts @@ -40,11 +40,15 @@ export async function closeAllCreatedServers() { } export function appModuleForControllers(controllers: ClassType[], entities: ClassType[] = []): AppModule { - const database = Database.createClass('default', new SQLiteDatabaseAdapter(), entities); + class MyDatabase extends Database { + constructor() { + super(new SQLiteDatabaseAdapter(), entities); + } + } return new AppModule({ controllers: controllers, providers: [ - { provide: Database, useClass: database }, + { provide: Database, useClass: MyDatabase }, { provide: Broker, useClass: NetBroker }, { provide: BrokerServer, useClass: NetBrokerServer }, ], diff --git a/packages/framework/src/application-server.ts b/packages/framework/src/application-server.ts index 101604769..f83abf868 100644 --- a/packages/framework/src/application-server.ts +++ b/packages/framework/src/application-server.ts @@ -11,7 +11,7 @@ import { asyncOperation, getClassName, urlJoin } from '@deepkit/core'; import { RpcClient } from '@deepkit/rpc'; import cluster from 'cluster'; -import { Router } from '@deepkit/http'; +import { HttpRouter } from '@deepkit/http'; import { BaseEvent, EventDispatcher, eventDispatcher, EventToken } from '@deepkit/event'; import { InjectorContext } from '@deepkit/injector'; import { FrameworkConfig } from './module.config'; @@ -81,7 +81,7 @@ type ApplicationServerConfig = Pick; -function needsHttpWorker(config: { publicDir?: string }, rpcControllers: RpcControllers, router: Router) { +function needsHttpWorker(config: { publicDir?: string }, rpcControllers: RpcControllers, router: HttpRouter) { return Boolean(config.publicDir || rpcControllers.controllers.size || router.getRoutes().length); } @@ -89,7 +89,7 @@ export class ApplicationServerListener { constructor( protected logger: LoggerInterface, protected rpcControllers: RpcControllers, - protected router: Router, + protected router: HttpRouter, protected config: ApplicationServerConfig, ) { } @@ -108,7 +108,7 @@ export class ApplicationServerListener { let lastController: any = undefined; for (const route of routes) { if (route.internal) continue; - if (lastController !== route.action.controller) { + if (route.action.type === 'controller' && lastController !== route.action.controller) { lastController = route.action.controller; this.logger.log(`HTTP Controller ${getClassName(lastController)}`); } @@ -153,7 +153,7 @@ export class ApplicationServer { protected rootScopedContext: InjectorContext, public config: ApplicationServerConfig, protected rpcControllers: RpcControllers, - protected router: Router, + protected router: HttpRouter, ) { this.needsHttpWorker = needsHttpWorker(config, rpcControllers, router); } diff --git a/packages/framework/src/broker/broker.ts b/packages/framework/src/broker/broker.ts index 1f4de1c0b..b3c66e28d 100644 --- a/packages/framework/src/broker/broker.ts +++ b/packages/framework/src/broker/broker.ts @@ -12,7 +12,7 @@ import { BrokerChannel, BrokerClient, BrokerKernel } from '@deepkit/broker'; import { ClassType } from '@deepkit/core'; import { IdInterface, RpcDirectClientAdapter } from '@deepkit/rpc'; import { BrokerConfig } from './broker.config'; -import { NetTcpRpcClientAdapter, NetTcpRpcServer, TcpRpcClientAdapter, TcpRpcServer } from '@deepkit/rpc-tcp'; +import { RpcNetTcpClientAdapter, RpcNetTcpServer, RpcTcpClientAdapter, RpcTcpServer } from '@deepkit/rpc-tcp'; import { MongoId, ReflectionClass, Type, typeOf, UUID } from '@deepkit/type'; @@ -90,13 +90,13 @@ export class BaseBroker extends BrokerClient { export class Broker extends BaseBroker { constructor(protected host: BrokerConfig['host']) { - super(new TcpRpcClientAdapter(host)); + super(new RpcTcpClientAdapter(host)); } } export class NetBroker extends BaseBroker { constructor(protected host: BrokerConfig['host']) { - super(new NetTcpRpcClientAdapter(host)); + super(new RpcNetTcpClientAdapter(host)); } } @@ -106,7 +106,7 @@ export class DirectBroker extends BaseBroker { } } -export class BrokerServer extends TcpRpcServer { +export class BrokerServer extends RpcTcpServer { protected kernel: BrokerKernel = new BrokerKernel; constructor(protected listen: BrokerConfig['listen']) { @@ -114,7 +114,7 @@ export class BrokerServer extends TcpRpcServer { } } -export class NetBrokerServer extends NetTcpRpcServer { +export class NetBrokerServer extends RpcNetTcpServer { protected kernel: BrokerKernel = new BrokerKernel; constructor(listen: BrokerConfig['listen']) { diff --git a/packages/framework/src/cli/debug-router.ts b/packages/framework/src/cli/debug-router.ts index 178637fc7..aa8a810e1 100644 --- a/packages/framework/src/cli/debug-router.ts +++ b/packages/framework/src/cli/debug-router.ts @@ -9,13 +9,13 @@ */ import { cli, Command } from '@deepkit/app'; -import { Router } from '@deepkit/http'; +import { HttpRouter } from '@deepkit/http'; @cli.controller('debug:router', { }) export class DebugRouterController implements Command { constructor( - protected router: Router, + protected router: HttpRouter, ) { } diff --git a/packages/framework/src/debug/broker.ts b/packages/framework/src/debug/broker.ts index 469805072..4a782a89e 100644 --- a/packages/framework/src/debug/broker.ts +++ b/packages/framework/src/debug/broker.ts @@ -1,19 +1,19 @@ import { BaseBroker } from '../broker/broker'; import { eventDispatcher } from '@deepkit/event'; import { onServerMainBootstrap, onServerMainShutdown } from '../application-server'; -import { NetTcpRpcClientAdapter, NetTcpRpcServer } from '@deepkit/rpc-tcp'; +import { RpcNetTcpClientAdapter, RpcNetTcpServer } from '@deepkit/rpc-tcp'; import { BrokerKernel } from '@deepkit/broker'; import { FrameworkConfig } from '../module.config'; export class DebugBroker extends BaseBroker { constructor(brokerHost: FrameworkConfig['debugBrokerHost']) { - super(new NetTcpRpcClientAdapter(brokerHost)); + super(new RpcNetTcpClientAdapter(brokerHost)); } } export class DebugBrokerListener { protected kernel: BrokerKernel = new BrokerKernel; - protected server = new NetTcpRpcServer(this.kernel, this.brokerHost); + protected server = new RpcNetTcpServer(this.kernel, this.brokerHost); constructor(protected brokerHost: FrameworkConfig['debugBrokerHost'], protected broker: DebugBroker) { } diff --git a/packages/framework/src/debug/debug.controller.ts b/packages/framework/src/debug/debug.controller.ts index eb81a15fc..77fbec878 100644 --- a/packages/framework/src/debug/debug.controller.ts +++ b/packages/framework/src/debug/debug.controller.ts @@ -23,7 +23,7 @@ import { Workflow } from '@deepkit/framework-debug-api'; import { rpc, rpcClass } from '@deepkit/rpc'; -import { parseRouteControllerAction, Router } from '@deepkit/http'; +import { parseRouteControllerAction, HttpRouter } from '@deepkit/http'; import { changeClass, ClassType, getClassName, isClass } from '@deepkit/core'; import { EventDispatcher, isEventListenerContainerEntryService } from '@deepkit/event'; import { DatabaseAdapter, DatabaseRegistry } from '@deepkit/orm'; @@ -46,7 +46,7 @@ export class DebugController implements DebugControllerInterface { constructor( protected serviceContainer: ServiceContainer, protected eventDispatcher: EventDispatcher, - protected router: Router, + protected router: HttpRouter, protected config: Pick, protected rpcControllers: RpcControllers, protected databaseRegistry: DatabaseRegistry, @@ -154,7 +154,7 @@ export class DebugController implements DebugControllerInterface { parameters: [], groups: route.groups, category: route.category, - controller: getClassName(route.action.controller) + '.' + route.action.methodName, + controller: route.action.type === 'controller' ? getClassName(route.action.controller) + '.' + route.action.methodName : route.action.fn.name, description: route.description, }; const parsedRoute = parseRouteControllerAction(route); diff --git a/packages/framework/src/module.ts b/packages/framework/src/module.ts index 4f9293dc0..b7982febc 100644 --- a/packages/framework/src/module.ts +++ b/packages/framework/src/module.ts @@ -64,7 +64,7 @@ export class FrameworkModule extends createModule({ } for (const [name, info] of rpcControllers.controllers.entries()) { - kernel.registerController(name, info.controller, info.module); + kernel.registerController(info.controller, name, info.module); } return kernel; diff --git a/packages/framework/tests/http.spec.ts b/packages/framework/tests/http.spec.ts new file mode 100644 index 000000000..8619fe387 --- /dev/null +++ b/packages/framework/tests/http.spec.ts @@ -0,0 +1,22 @@ +import { App } from '@deepkit/app'; +import { FrameworkModule } from '../src/module'; +import { expect, test } from '@jest/globals'; +import { HttpKernel, HttpRequest, HttpRouterRegistry } from '@deepkit/http'; +import { Logger } from '@deepkit/logger'; + +test('functional http app', async () => { + const app = new App({ + imports: [new FrameworkModule()] + }); + + const router = app.get(HttpRouterRegistry); + + router.get('/greet/:name', (name: string, logger: Logger) => { + logger.log(`${name} was greeted`); + return `Greetings ${name}`; + }); + + const httpKernel = app.get(HttpKernel); + const response = await httpKernel.request(HttpRequest.GET('/greet/Peter')); + expect(response.json).toBe('Greetings Peter'); +}); diff --git a/packages/framework/tests/service-container.spec.ts b/packages/framework/tests/service-container.spec.ts index 0e7c101c9..e1a193573 100644 --- a/packages/framework/tests/service-container.spec.ts +++ b/packages/framework/tests/service-container.spec.ts @@ -101,7 +101,7 @@ test('controller in module and overwrite service', () => { test('database auto-detection', () => { class MyDatabase extends Database { constructor() { - super(new MemoryDatabaseAdapter()) + super(new MemoryDatabaseAdapter()); } } @@ -112,5 +112,25 @@ test('database auto-detection', () => { const registry = app.get(DatabaseRegistry); expect(registry.getDatabases()).toHaveLength(1); +}); + +test('database injection', () => { + class Service { + constructor(public db: Database) { + } + } + const app = new App({ + imports: [new FrameworkModule({ debug: true })], + providers: [Service, { provide: Database, useValue: new Database(new MemoryDatabaseAdapter()) }], + }); + + const db = app.get(Database); + expect(db).toBeInstanceOf(Database); + + const service = app.get(Service); + expect(service.db).toBeInstanceOf(Database); + + const registry = app.get(DatabaseRegistry); + expect(registry.getDatabases()).toHaveLength(1); }); diff --git a/packages/http/package.json b/packages/http/package.json index 874d2a3b7..715738ead 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -54,6 +54,9 @@ "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "resolver": "../../jest-resolver.js", "testMatch": [ "**/tests/**/*.spec.ts" diff --git a/packages/http/src/decorator.ts b/packages/http/src/decorator.ts index dbf81bcc3..522a9ad6f 100644 --- a/packages/http/src/decorator.ts +++ b/packages/http/src/decorator.ts @@ -36,7 +36,7 @@ function isMiddlewareClassTypeOrFn(v: HttpActionMiddleware): v is ClassType(); groups: string[] = []; @@ -110,7 +110,7 @@ export class HttpAction { responses: { statusCode: number, description: string, type?: Type }[] = []; } -export class HttpDecorator { +export class HttpControllerDecorator { t = new HttpController; controller(baseUrl: string = '') { @@ -182,13 +182,14 @@ export class HttpDecorator { } } -export const httpClass: ClassDecoratorResult = createClassDecoratorContext(HttpDecorator); +export const httpClass: ClassDecoratorResult = createClassDecoratorContext(HttpControllerDecorator); export class HttpActionDecorator { t = new HttpAction; onDecorator(target: ClassType, property: string | undefined, parameterIndexOrDescriptor?: any) { if (!property) return; + if (target === Object) return; this.t.methodName = property; httpClass.setAction(this.t)(target); } @@ -315,17 +316,17 @@ export class HttpActionDecorator { * * ```typescript * - * @http.GET().response(200, 'All ok') + * http.GET().response(200, 'All ok') * * interface User { * username: string; * } - * @http.GET().response(200, 'User object') + * http.GET().response(200, 'User object') * * interface HttpErrorMessage { * error: string; * } - * @http.GET().response(500, 'Error') + * http.GET().response(500, 'Error') * ``` */ response(statusCode: number, description: string = '', type?: ReceiveType) { @@ -397,4 +398,6 @@ export const httpAction: HttpActionPropertyDecoratorResult = createPropertyDecor type HttpMerge = { [K in keyof U]: K extends 'response' ? (statusCode: number, description?: string, type?: ReceiveType) => PropertyDecoratorFn & U : U[K] extends ((...a: infer A) => infer R) ? R extends DualDecorator ? (...a: A) => PropertyDecoratorFn & R & U : (...a: A) => R : never }; type MergedHttp = HttpMerge, '_fetch' | 't'>> +export type HttpDecorator = PropertyDecoratorFn & HttpActionFluidDecorator; + export const http: MergedHttp<[typeof httpClass, typeof httpAction]> = mergeDecorator(httpClass, httpAction) as any as MergedHttp<[typeof httpClass, typeof httpAction]>; diff --git a/packages/http/src/filter.ts b/packages/http/src/filter.ts index 625a7096a..5252ba4f1 100644 --- a/packages/http/src/filter.ts +++ b/packages/http/src/filter.ts @@ -1,6 +1,6 @@ import { ClassType } from '@deepkit/core'; import { AppModule } from '@deepkit/app'; -import { RouteConfig, Router } from './router'; +import { RouteConfig, HttpRouter } from './router'; export interface HttpRouteFilterRoute { path?: string; @@ -57,7 +57,7 @@ function match(routeConfig: RouteConfig, route: HttpRouteFilterRoute): boolean { } export class HttpRouterFilterResolver { - constructor(protected router: Router) { + constructor(protected router: HttpRouter) { } /** @@ -68,9 +68,9 @@ export class HttpRouterFilterResolver { outer: for (const routeConfig of this.router.getRoutes()) { - if (filter.controllers.length && !filter.controllers.includes(routeConfig.action.controller)) continue; + if (filter.controllers.length && routeConfig.action.type === 'controller' && !filter.controllers.includes(routeConfig.action.controller)) continue; - if (filter.excludeControllers.length && filter.excludeControllers.includes(routeConfig.action.controller)) continue; + if (filter.excludeControllers.length && routeConfig.action.type === 'controller' && filter.excludeControllers.includes(routeConfig.action.controller)) continue; if (filter.modules.length) { if (!routeConfig.module) continue; diff --git a/packages/http/src/http.ts b/packages/http/src/http.ts index 82a7d47f1..2c3fe867e 100644 --- a/packages/http/src/http.ts +++ b/packages/http/src/http.ts @@ -14,7 +14,7 @@ import { eventDispatcher } from '@deepkit/event'; import { HttpRequest, HttpResponse } from './model'; import { InjectorContext } from '@deepkit/injector'; import { LoggerInterface } from '@deepkit/logger'; -import { RouteConfig, RouteParameterResolverForInjector, Router } from './router'; +import { HttpRouter, RouteConfig, RouteParameterResolverForInjector } from './router'; import { createWorkflow, WorkflowEvent } from '@deepkit/workflow'; import type { ElementStruct, render } from '@deepkit/template'; import { FrameCategory, Stopwatch } from '@deepkit/stopwatch'; @@ -336,6 +336,10 @@ export class BaseResponse { return this; } + /** + * Per default a JSONResponse is serialized using the return type specified at the route. + * This disables that behaviour so that JSON.stringify is run on the result directly. + */ disableAutoSerializing() { this.autoSerializing = false; return this; @@ -357,6 +361,13 @@ export class BaseResponse { } } +export class Response extends BaseResponse { + constructor(public content: string | Uint8Array, contentType: string, statusCode?: number) { + super(statusCode); + this.contentType(contentType); + } +} + export class HtmlResponse extends BaseResponse { constructor(public html: string, statusCode?: number) { super(statusCode); @@ -369,7 +380,7 @@ export class JSONResponse extends BaseResponse { } } -export type SupportedHttpResult = undefined | null | number | string | JSONResponse | HtmlResponse | HttpResponse | ServerResponse | Redirect | Uint8Array | Error; +export type SupportedHttpResult = undefined | null | number | string | Response | JSONResponse | HtmlResponse | HttpResponse | ServerResponse | Redirect | Uint8Array | Error; export interface HttpResultFormatterContext { request: HttpRequest; @@ -381,7 +392,7 @@ export class HttpResultFormatter { protected jsonContentType: string = 'application/json; charset=utf-8'; protected htmlContentType: string = 'text/html; charset=utf-8'; - constructor(protected router: Router) { + constructor(protected router: HttpRouter) { } protected setContentTypeIfNotSetAlready(response: HttpResponse, contentType: string): void { @@ -423,6 +434,12 @@ export class HttpResultFormatter { context.response.end(result.html); } + handleGenericResponse(result: Response, context: HttpResultFormatterContext): void { + context.response.writeHead(result._statusCode || 200, result._headers); + console.log('generic response', result.content); + context.response.end(result.content); + } + handleJSONResponse(result: JSONResponse, context: HttpResultFormatterContext): void { this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType); context.response.writeHead(result._statusCode || 200, result._headers); @@ -459,6 +476,8 @@ export class HttpResultFormatter { this.handleBinary(result, context); } else if (result instanceof JSONResponse) { this.handleJSONResponse(result, context); + } else if (result instanceof Response) { + this.handleGenericResponse(result, context); } else { if (isClassInstance(result)) { const classType = getClassTypeFromInstance(result); @@ -475,7 +494,7 @@ export class HttpResultFormatter { export class HttpListener { constructor( - protected router: Router, + protected router: HttpRouter, protected logger: LoggerInterface, protected resultFormatter: HttpResultFormatter, protected stopwatch?: Stopwatch, @@ -611,13 +630,22 @@ export class HttpListener { if (event.sent) return; if (event.hasNext()) return; - const controllerInstance = event.injectorContext.get(event.route.action.controller, event.route.action.module); const start = Date.now(); - const frame = this.stopwatch ? this.stopwatch.start(getClassName(event.route.action.controller) + '.' + event.route.action.methodName, FrameCategory.httpController) : undefined; + const stopWatchLabel = event.route.action.type === 'controller' + ? getClassName(event.route.action.controller) + '.' + event.route.action.methodName + : event.route.action.fn.name; + + const frame = this.stopwatch ? this.stopwatch.start(stopWatchLabel, FrameCategory.httpController) : undefined; try { - const method = controllerInstance[event.route.action.methodName]; - let result = await method.apply(controllerInstance, event.parameters); + let result: any; + if (event.route.action.type === 'controller') { + const controllerInstance = event.injectorContext.get(event.route.action.controller, event.route.action.module); + const method = controllerInstance[event.route.action.methodName]; + result = await method.apply(controllerInstance, event.parameters); + } else { + result = await event.route.action.fn(...event.parameters); + } if (isElementStruct(result)) { const html = await getTemplateRender()(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined); diff --git a/packages/http/src/kernel.ts b/packages/http/src/kernel.ts index 293a6fd9d..b1bf58281 100644 --- a/packages/http/src/kernel.ts +++ b/packages/http/src/kernel.ts @@ -1,5 +1,5 @@ import { InjectorContext } from '@deepkit/injector'; -import { Router } from './router'; +import { HttpRouter } from './router'; import { EventDispatcher } from '@deepkit/event'; import { LoggerInterface } from '@deepkit/logger'; import { HttpRequest, HttpResponse, MemoryHttpResponse, RequestBuilder } from './model'; @@ -9,7 +9,7 @@ import { unlink } from 'fs'; export class HttpKernel { constructor( - protected router: Router, + protected router: HttpRouter, protected eventDispatcher: EventDispatcher, protected injectorContext: InjectorContext, protected logger: LoggerInterface, diff --git a/packages/http/src/model.ts b/packages/http/src/model.ts index 535a90324..2767d2523 100644 --- a/packages/http/src/model.ts +++ b/packages/http/src/model.ts @@ -8,11 +8,12 @@ * You should have received a copy of the MIT License along with this program. */ -import { IncomingMessage, ServerResponse } from 'http'; +import { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'http'; import { UploadedFile } from './router'; import * as querystring from 'querystring'; import { Writable } from 'stream'; import { metaAnnotation, ReflectionKind, Type, ValidationErrorItem } from '@deepkit/type'; +import { isArray } from '@deepkit/core'; export class HttpResponse extends ServerResponse { status(code: number) { @@ -235,6 +236,33 @@ export class HttpRequest extends IncomingMessage { export class MemoryHttpResponse extends HttpResponse { public body: Buffer = Buffer.alloc(0); + public headers: {[name: string]: number | string | string[] | undefined} = Object.create(null); + + setHeader(name: string, value: number | string | ReadonlyArray) { + this.headers[name] = value as any; + super.setHeader(name, value); + } + + removeHeader(name: string) { + delete this.headers[name]; + super.removeHeader(name); + } + + getHeader(name: string) { + return this.headers[name]; + } + + getHeaders(): OutgoingHttpHeaders { + return this.headers; + } + + writeHead(statusCode: number, headersOrReasonPhrase?: string | OutgoingHttpHeaders | OutgoingHttpHeader[], headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this { + headers = typeof headersOrReasonPhrase === 'string' ? headers : headersOrReasonPhrase; + if (headers && !isArray(headers)) this.headers = headers; + + if (typeof headersOrReasonPhrase === 'string') return super.writeHead(statusCode, headersOrReasonPhrase, headers); + return super.writeHead(statusCode, headers); + } get json(): any { const json = this.bodyString; @@ -287,6 +315,5 @@ export class MemoryHttpResponse extends HttpResponse { this.body = Buffer.concat([this.body, chunk]); } return super.end(chunk, encoding, callback); - // if (callback) callback(); } } diff --git a/packages/http/src/module.ts b/packages/http/src/module.ts index d7dacdded..3b438f7b2 100644 --- a/packages/http/src/module.ts +++ b/packages/http/src/module.ts @@ -1,7 +1,7 @@ import { HttpListener, HttpResultFormatter, httpWorkflow } from './http'; import { HttpConfig } from './module.config'; import { AppModule, createModule } from '@deepkit/app'; -import { Router } from './router'; +import { HttpRouter, HttpRouterRegistry } from './router'; import { HttpKernel } from './kernel'; import { HttpRouterFilterResolver } from './filter'; import { HttpControllers } from './controllers'; @@ -14,9 +14,10 @@ import { httpClass } from './decorator'; export class HttpModule extends createModule({ config: HttpConfig, providers: [ - Router, + HttpRouter, HttpKernel, HttpResultFormatter, + HttpRouterRegistry, HttpRouterFilterResolver, { provide: HttpResponse, scope: 'http' }, { provide: HttpRequest, scope: 'http' }, @@ -29,7 +30,8 @@ export class HttpModule extends createModule({ httpWorkflow ], exports: [ - Router, + HttpRouter, + HttpRouterRegistry, HttpKernel, HttpResultFormatter, HttpRouterFilterResolver, diff --git a/packages/http/src/router.ts b/packages/http/src/router.ts index ecf0444fb..e6a025e68 100644 --- a/packages/http/src/router.ts +++ b/packages/http/src/router.ts @@ -16,6 +16,7 @@ import { getValidatorFunction, metaAnnotation, ReflectionClass, + ReflectionFunction, ReflectionKind, ReflectionParameter, SerializationOptions, @@ -29,7 +30,7 @@ import { // @ts-ignore import formidable from 'formidable'; import querystring from 'querystring'; -import { httpClass } from './decorator'; +import { HttpAction, httpClass, HttpController, HttpDecorator } from './decorator'; import { BodyValidationError, getRegExp, HttpRequest, HttpRequestQuery, HttpRequestResolvedParameters, ValidatedBody } from './model'; import { InjectorContext, InjectorModule, TagRegistry } from '@deepkit/injector'; import { Logger, LoggerInterface } from '@deepkit/logger'; @@ -39,6 +40,7 @@ import { HttpMiddlewareConfig, HttpMiddlewareFn } from './middleware'; //@ts-ignore import qs from 'qs'; +import { isArray } from '@deepkit/core'; export type RouteParameterResolverForInjector = ((injector: InjectorContext) => any[] | Promise); @@ -83,15 +85,19 @@ export class UploadedFile { // hash!: string | 'sha1' | 'md5' | 'sha256' | null; } -export interface RouteControllerAction { +export interface RouteFunctionControllerAction { + type: 'function'; //if not set, the root module is used module?: InjectorModule; - controller: ClassType; - methodName: string; + fn: (...args: any[]) => any; } -function getRouterControllerActionName(action: RouteControllerAction): string { - return `${getClassName(action.controller)}.${action.methodName}`; +export interface RouteClassControllerAction { + type: 'controller'; + //if not set, the root module is used + module?: InjectorModule; + controller: ClassType; + methodName: string; } export class RouteConfig { @@ -115,7 +121,7 @@ export class RouteConfig { resolverForToken: Map = new Map(); - middlewares: { config: HttpMiddlewareConfig, module: InjectorModule }[] = []; + middlewares: { config: HttpMiddlewareConfig, module?: InjectorModule }[] = []; resolverForParameterName: Map = new Map(); @@ -128,11 +134,17 @@ export class RouteConfig { public readonly name: string, public readonly httpMethods: string[], public readonly path: string, - public readonly action: RouteControllerAction, + public readonly action: RouteClassControllerAction | RouteFunctionControllerAction, public internal: boolean = false, ) { } + getReflectionFunction(): ReflectionFunction { + return this.action.type === 'controller' ? + ReflectionClass.from(this.action.controller).getMethod(this.action.methodName) + : ReflectionFunction.from(this.action.fn); + } + getSchemaForResponse(statusCode: number): Type | undefined { if (!this.responses.length) return; for (const response of this.responses) { @@ -231,13 +243,13 @@ function parseRoutePathToRegex(routeConfig: RouteConfig): { regex: string, param const parameterNames: { [name: string]: number } = {}; let path = routeConfig.getFullPath(); - const method = ReflectionClass.from(routeConfig.action.controller).getMethod(routeConfig.action.methodName); + const fn = routeConfig.getReflectionFunction(); let argumentIndex = 0; path = path.replace(/:(\w+)/g, (a, name) => { parameterNames[name] = argumentIndex; argumentIndex++; - const parameter = method.getParameterOrUndefined(name); + const parameter = fn.getParameterOrUndefined(name); if (parameter) { const regExp = getRegExp(parameter.type); if (regExp) return '(' + regExp + ')'; @@ -249,10 +261,9 @@ function parseRoutePathToRegex(routeConfig: RouteConfig): { regex: string, param } export function parseRouteControllerAction(routeConfig: RouteConfig): ParsedRoute { - const schema = ReflectionClass.from(routeConfig.action.controller); const parsedRoute = new ParsedRoute(routeConfig); - const methodArgumentProperties = schema.getMethodParameters(routeConfig.action.methodName); + const methodArgumentProperties = routeConfig.getReflectionFunction().getParameters(); const parsedPath = parseRoutePathToRegex(routeConfig); parsedRoute.regex = parsedPath.regex; parsedRoute.pathParameterNames = parsedPath.parameterNames; @@ -302,11 +313,11 @@ function filterMiddlewaresForRoute(middlewareRawConfigs: MiddlewareRegistryEntry const middlewareConfigs = middlewares.filter((v) => { if (!(v.config instanceof HttpMiddlewareConfig)) return false; - if (v.config.controllers.length && !v.config.controllers.includes(routeConfig.action.controller)) { + if (v.config.controllers.length && routeConfig.action.type === 'controller' && !v.config.controllers.includes(routeConfig.action.controller)) { return false; } - if (v.config.excludeControllers.length && v.config.excludeControllers.includes(routeConfig.action.controller)) { + if (v.config.excludeControllers.length && routeConfig.action.type === 'controller' && v.config.excludeControllers.includes(routeConfig.action.controller)) { return false; } @@ -366,11 +377,231 @@ function filterMiddlewaresForRoute(middlewareRawConfigs: MiddlewareRegistryEntry return middlewareConfigs; } -export class Router { - protected fn?: (request: HttpRequest) => ResolvedController | undefined; - protected resolveFn?: (name: string, parameters: { [name: string]: any }) => string; +export interface HttpRouterFunctionOptions { + path: string; + name?: string; + methods?: string[]; + description?: string; + category?: string; + groups?: string[]; + + /** + * An arbitrary data container the user can use to store app specific settings/values. + */ + data?: Record; + + baseUrl?: string; + middlewares?: (() => HttpMiddlewareConfig)[]; + + serializer?: Serializer; + serializationOptions?: SerializationOptions; + + resolverForToken?: Map; + resolverForParameterName?: Map; + + responses?: { statusCode: number, description: string, type?: Type }[]; +} + +function convertOptions(methods: string[], pathOrOptions: string | HttpRouterFunctionOptions, defaultOptions: Partial): HttpRouterFunctionOptions { + const options = 'string' === typeof pathOrOptions ? { path: pathOrOptions } : pathOrOptions; + if (options.methods) return options; + return { ...options, methods }; +} + +export abstract class HttpRouterRegistryFunctionRegistrar { + protected defaultOptions: Partial = {}; + + abstract addRoute(routeConfig: RouteConfig): void; + + /** + * Returns a new registrar object with default options that apply to each registered route through this registrar. + * + * ```typescript + * const registry: HttpRouterRegistry = ...; + * + * const secretRegistry = registry.forOptions({groups: ['secret']}); + * + * secretRegistry.get('/admin/groups', () => { + * }); + * + * secretRegistry.get('/admin/users', () => { + * }); + * ``` + */ + forOptions(options: Partial): HttpRouterRegistryFunctionRegistrar { + const that = this; + return new class extends HttpRouterRegistryFunctionRegistrar { + defaultOptions = options; + + addRoute(routeConfig: RouteConfig) { + that.addRoute(routeConfig); + } + }; + } + + public any(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions([], pathOrOptions, this.defaultOptions), callback); + } + + public add(decorator: HttpDecorator, callback: (...args: any[]) => any) { + const data = decorator(Object, '_'); + const action = isArray(data) ? data.find(v => v instanceof HttpAction) : undefined; + if (!action) throw new Error('No HttpAction available'); + + const fn = ReflectionFunction.from(callback); + const routeConfig = createRouteConfigFromHttpAction({ + type: 'function', + fn: callback, + }, action); + routeConfig.returnType = fn.getReturnType(); + this.addRoute(routeConfig); + } + + public get(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['GET'], pathOrOptions, this.defaultOptions), callback); + } + + public post(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['POST'], pathOrOptions, this.defaultOptions), callback); + } + + public put(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['PUT'], pathOrOptions, this.defaultOptions), callback); + } + + public patch(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['PATCH'], pathOrOptions, this.defaultOptions), callback); + } + + public delete(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['DELETE'], pathOrOptions, this.defaultOptions), callback); + } + + public options(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['OPTIONS'], pathOrOptions, this.defaultOptions), callback); + } + public trace(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['TRACE'], pathOrOptions, this.defaultOptions), callback); + } + + public head(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) { + this.register(convertOptions(['HEAD'], pathOrOptions, this.defaultOptions), callback); + } + + private register(options: HttpRouterFunctionOptions, callback: (...args: any[]) => any, module?: InjectorModule) { + const fn = ReflectionFunction.from(callback); + + const routeConfig = new RouteConfig(options.name || '', options.methods || [], options.path, { + type: 'function', + fn: callback, + }); + routeConfig.module = module; + if (options.responses) routeConfig.responses = options.responses; + if (options.description) routeConfig.description = options.description; + if (options.category) routeConfig.category = options.category; + if (options.groups) routeConfig.groups = options.groups; + if (options.data) routeConfig.data = new Map(Object.entries(options.data)); + if (options.baseUrl) routeConfig.baseUrl = options.baseUrl; + if (options.middlewares) { + routeConfig.middlewares = options.middlewares.map(v => { + return { config: v(), module }; + }); + } + + if (options.resolverForToken) { + for (const item of options.resolverForToken) routeConfig.resolverForToken.set(...item); + } + + if (options.resolverForToken) { + for (const item of options.resolverForToken) routeConfig.resolverForToken.set(...item); + } + + if (options.resolverForParameterName) { + for (const item of options.resolverForParameterName) routeConfig.resolverForParameterName.set(...item); + } + + routeConfig.serializer = options.serializer; + routeConfig.serializationOptions = options.serializationOptions; + + routeConfig.returnType = fn.getReturnType(); + this.addRoute(routeConfig); + } +} + +function createRouteConfigFromHttpAction(routeAction: RouteClassControllerAction | RouteFunctionControllerAction, action: HttpAction, module?: InjectorModule, controller?: HttpController) { + const routeConfig = new RouteConfig(action.name, action.httpMethods, action.path, routeAction); + routeConfig.responses = action.responses; + routeConfig.description = action.description; + routeConfig.category = action.category; + routeConfig.groups = action.groups; + routeConfig.data = new Map(action.data); + if (controller) { + routeConfig.baseUrl = controller.baseUrl; + + routeConfig.middlewares = controller.middlewares.map(v => { + return { config: v(), module }; + }); + routeConfig.resolverForToken = new Map(controller.resolverForToken); + routeConfig.resolverForParameterName = new Map(controller.resolverForParameterName); + } + + routeConfig.middlewares.push(...action.middlewares.map(v => { + return { config: v(), module }; + })); + + for (const item of action.resolverForToken) routeConfig.resolverForToken.set(...item); + + for (const item of action.resolverForParameterName) routeConfig.resolverForParameterName.set(...item); + + routeConfig.serializer = action.serializer; + routeConfig.serializationOptions = action.serializationOptions; + return routeConfig; +} + +export class HttpRouterRegistry extends HttpRouterRegistryFunctionRegistrar { protected routes: RouteConfig[] = []; + private buildId: number = 1; + + public getBuildId(): number { + return this.buildId; + } + + public getRoutes(): RouteConfig[] { + return this.routes; + } + + public addRouteForController(controller: ClassType, module: InjectorModule) { + const controllerData = httpClass._fetch(controller); + if (!controllerData) throw new Error(`Http controller class ${getClassName(controller)} has no @http.controller decorator.`); + const schema = ReflectionClass.from(controller); + + for (const action of controllerData.getActions()) { + const routeAction: RouteClassControllerAction = { + type: 'controller', + controller, + module, + methodName: action.methodName + }; + const routeConfig = createRouteConfigFromHttpAction(routeAction, action, module, controllerData); + + routeConfig.module = module; + + if (schema.hasMethod(action.methodName)) routeConfig.returnType = schema.getMethod(action.methodName).getReturnType(); + this.addRoute(routeConfig); + } + } + + public addRoute(routeConfig: RouteConfig) { + this.routes.push(routeConfig); + this.buildId++; + } +} + +export class HttpRouter { + protected fn?: (request: HttpRequest) => ResolvedController | undefined; + protected buildId: number = 0; + protected resolveFn?: (name: string, parameters: { [name: string]: any }) => string; private parseBody(req: HttpRequest, files: { [name: string]: UploadedFile }) { const form = formidable({ @@ -398,6 +629,7 @@ export class Router { private logger: LoggerInterface, tagRegistry: TagRegistry, private middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry, + private registry: HttpRouterRegistry = new HttpRouterRegistry, ) { for (const controller of controllers.controllers) { this.addRouteForController(controller.controller, controller.module); @@ -405,7 +637,7 @@ export class Router { } getRoutes(): RouteConfig[] { - return this.routes; + return this.registry.getRoutes(); } static forControllers( @@ -413,7 +645,7 @@ export class Router { tagRegistry: TagRegistry = new TagRegistry(), middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry(), module: InjectorModule = new InjectorModule() - ): Router { + ): HttpRouter { return new this(new HttpControllers(controllers.map(v => { return isClass(v) ? { controller: v, module } : v; })), new Logger([], []), tagRegistry, middlewareRegistry); @@ -640,60 +872,22 @@ export class Router { } public addRoute(routeConfig: RouteConfig) { - this.routes.push(routeConfig); - this.fn = undefined; + this.registry.addRoute(routeConfig); } public addRouteForController(controller: ClassType, module: InjectorModule) { - const data = httpClass._fetch(controller); - if (!data) throw new Error(`Http controller class ${getClassName(controller)} has no @http.controller decorator.`); - const schema = ReflectionClass.from(controller); - - for (const action of data.getActions()) { - const routeConfig = new RouteConfig(action.name, action.httpMethods, action.path, { - controller, - module, - methodName: action.methodName - }); - routeConfig.module = module; - routeConfig.responses = action.responses; - routeConfig.description = action.description; - routeConfig.category = action.category; - routeConfig.groups = action.groups; - routeConfig.data = new Map(action.data); - routeConfig.baseUrl = data.baseUrl; - - routeConfig.middlewares = data.middlewares.map(v => { - return { config: v(), module }; - }); - routeConfig.middlewares.push(...action.middlewares.map(v => { - return { config: v(), module }; - })); - - for (const item of action.resolverForToken) routeConfig.resolverForToken.set(...item); - - routeConfig.resolverForToken = new Map(data.resolverForToken); - for (const item of action.resolverForToken) routeConfig.resolverForToken.set(...item); - - routeConfig.resolverForParameterName = new Map(data.resolverForParameterName); - for (const item of action.resolverForParameterName) routeConfig.resolverForParameterName.set(...item); - - routeConfig.serializationOptions = action.serializationOptions; - routeConfig.serializer = action.serializer; - routeConfig.serializer = action.serializer; - if (schema.hasMethod(action.methodName)) routeConfig.returnType = schema.getMethod(action.methodName).getReturnType(); - this.addRoute(routeConfig); - } + this.registry.addRouteForController(controller, module); } protected build(): (request: HttpRequest) => ResolvedController | undefined { + this.buildId = this.registry.getBuildId(); const compiler = new CompilerContext; compiler.context.set('ValidationError', ValidationError); compiler.context.set('qs', qs); const code: string[] = []; - for (const route of this.routes) { + for (const route of this.getRoutes()) { code.push(this.getRouteCode(compiler, route)); } @@ -713,7 +907,7 @@ export class Router { const compiler = new CompilerContext; const code: string[] = []; - for (const route of this.routes) { + for (const route of this.getRoutes()) { code.push(this.getRouteUrlResolveCode(compiler, route)); } @@ -734,7 +928,7 @@ export class Router { } public resolveRequest(request: HttpRequest): ResolvedController | undefined { - if (!this.fn) { + if (!this.fn || this.buildId !== this.registry.getBuildId()) { this.fn = this.build(); } diff --git a/packages/http/src/static-serving.ts b/packages/http/src/static-serving.ts index d66821dd5..732efc6ad 100644 --- a/packages/http/src/static-serving.ts +++ b/packages/http/src/static-serving.ts @@ -18,7 +18,7 @@ import { ClassType, urlJoin } from '@deepkit/core'; import { HttpRequest, HttpResponse } from './model'; import send from 'send'; import { eventDispatcher } from '@deepkit/event'; -import { RouteConfig, Router } from './router'; +import { RouteConfig, HttpRouter } from './router'; export function serveStaticListener(module: AppModule, path: string, localPath: string = path): ClassType { class HttpRequestStaticServingListener { @@ -45,6 +45,7 @@ export function serveStaticListener(module: AppModule, path: string, localP if (stat && stat.isFile()) { event.routeFound( new RouteConfig('static', ['GET'], event.url, { + type: 'controller', controller: HttpRequestStaticServingListener, module, methodName: 'serve' @@ -128,21 +129,23 @@ export function registerStaticHttpController(module: AppModule, options: St const path = normalizeDirectory(options.path); const route1 = new RouteConfig('static', ['GET'], path, { + type: 'controller', controller: StaticController, module, methodName: 'serveIndex' }); route1.groups = groups; - module.setupGlobalProvider().addRoute(route1); + module.setupGlobalProvider().addRoute(route1); if (path !== '/') { const route2 = new RouteConfig('static', ['GET'], path.slice(0, -1), { + type: 'controller', controller: StaticController, module, methodName: 'serveIndex' }); route2.groups = groups; - module.setupGlobalProvider().addRoute(route2); + module.setupGlobalProvider().addRoute(route2); } module.addProvider(StaticController); diff --git a/packages/http/tests/filter.spec.ts b/packages/http/tests/filter.spec.ts index bf3af76a8..dcf70f29b 100644 --- a/packages/http/tests/filter.spec.ts +++ b/packages/http/tests/filter.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@jest/globals'; -import { Router } from '../src/router'; +import { HttpRouter } from '../src/router'; import { HttpRouteFilter, HttpRouterFilterResolver } from '../src/filter'; import { http } from '../src/decorator'; import { createModule } from '@deepkit/app'; @@ -17,7 +17,7 @@ test('filter by controller', async () => { } } - const resolver = new HttpRouterFilterResolver(Router.forControllers([ + const resolver = new HttpRouterFilterResolver(HttpRouter.forControllers([ ControllerA, ControllerB ])); @@ -49,7 +49,7 @@ test('filter by route names', async () => { } } - const resolver = new HttpRouterFilterResolver(Router.forControllers([ + const resolver = new HttpRouterFilterResolver(HttpRouter.forControllers([ ControllerA, ControllerB ])); @@ -79,7 +79,7 @@ test('filter by route names and controller', async () => { } } - const resolver = new HttpRouterFilterResolver(Router.forControllers([ + const resolver = new HttpRouterFilterResolver(HttpRouter.forControllers([ ControllerA, ControllerB ])); @@ -105,7 +105,7 @@ test('filter by groups', async () => { } } - const resolver = new HttpRouterFilterResolver(Router.forControllers([ + const resolver = new HttpRouterFilterResolver(HttpRouter.forControllers([ ControllerA, ControllerB ])); @@ -156,7 +156,7 @@ test('filter by modules', async () => { expect(moduleB instanceof ModuleA).toBe(false); expect(moduleB instanceof ModuleB).toBe(true); - const resolver = new HttpRouterFilterResolver(Router.forControllers([ + const resolver = new HttpRouterFilterResolver(HttpRouter.forControllers([ { controller: ControllerA, module: moduleA }, { controller: ControllerB, module: moduleA }, { controller: ControllerC, module: moduleB }, ])); diff --git a/packages/http/tests/middleware.spec.ts b/packages/http/tests/middleware.spec.ts index 2372038ce..c36662583 100644 --- a/packages/http/tests/middleware.spec.ts +++ b/packages/http/tests/middleware.spec.ts @@ -21,7 +21,7 @@ test('middleware empty', async () => { const response = await httpKernel.request(HttpRequest.GET('/user/name1')); expect(response.statusCode).toEqual(200); expect(response.bodyString).toEqual('"name1"'); - expect(response.getHeader('content-type')).toEqual('application/json; charset=utf-8'); + expect(response.getHeader('Content-Type')).toEqual('application/json; charset=utf-8'); }); test('middleware async success', async () => { @@ -265,7 +265,7 @@ test('middleware keep content type', async () => { const response = await httpKernel.request(HttpRequest.GET('/user/name1')); expect(response.statusCode).toEqual(200); expect(response.bodyString).toEqual('"name1"'); - expect(response.getHeader('content-type')).toEqual('text/plain'); + expect(response.getHeader('Content-Type')).toEqual('text/plain'); }); test('middleware order natural', async () => { diff --git a/packages/http/tests/module.spec.ts b/packages/http/tests/module.spec.ts index c30872349..bf8eba04f 100644 --- a/packages/http/tests/module.spec.ts +++ b/packages/http/tests/module.spec.ts @@ -4,6 +4,7 @@ import { HttpModule } from '../src/module'; import { HttpKernel } from '../src/kernel'; import { HttpRequest } from '../src/model'; import { http } from '../src/decorator'; +import { httpWorkflow } from '../src/http.js'; test('module basic functionality', async () => { class Controller { @@ -30,3 +31,43 @@ test('module basic functionality', async () => { expect(response.json).toContain('hi'); } }); + +test('functional listener', async () => { + class Controller { + @http.GET('/hello/:name') + hello(name: string) { + return name; + } + } + + const gotUrls: string[] = []; + const app = new App({ + controllers: [ + Controller, + ], + listeners: [ + httpWorkflow.onController.listen(event => { + gotUrls.push(event.request.url || ''); + }), + ], + imports: [ + new HttpModule(), + ] + }); + + const httpKernel = app.get(HttpKernel); + + { + const response = await httpKernel.request(HttpRequest.GET('/hello/peter')); + expect(response.statusCode).toBe(200); + expect(response.json).toBe('peter'); + expect(gotUrls).toEqual(['/hello/peter']); + } + + { + const response = await httpKernel.request(HttpRequest.GET('/hello/marie')); + expect(response.statusCode).toBe(200); + expect(response.json).toBe('marie'); + expect(gotUrls).toEqual(['/hello/peter', '/hello/marie']); + } +}); diff --git a/packages/http/tests/router-functional.spec.ts b/packages/http/tests/router-functional.spec.ts new file mode 100644 index 000000000..403611741 --- /dev/null +++ b/packages/http/tests/router-functional.spec.ts @@ -0,0 +1,87 @@ +import { expect, test } from '@jest/globals'; +import { createHttpApp, createHttpKernel } from './utils.js'; +import { HttpRouter, HttpRouterRegistry } from '../src/router.js'; +import { HttpBody, HttpRequest } from '../src/model.js'; +import { http, HttpDecorator } from '../src/decorator.js'; +import { HttpKernel } from '../src/kernel.js'; + +test('router basics', async () => { + const httpKernel = createHttpKernel((registry: HttpRouterRegistry) => { + registry.get('/', () => { + return 'Hello World'; + }); + registry.get('/:text', (text: string) => { + return 'Hello ' + text; + }); + }); + + expect((await httpKernel.request(HttpRequest.GET('/'))).json).toEqual('Hello World'); + expect((await httpKernel.request(HttpRequest.GET('/Galaxy'))).json).toEqual('Hello Galaxy'); +}); + +test('router di', async () => { + const httpKernel = createHttpKernel((registry: HttpRouterRegistry) => { + registry.get('/:text', (text: string, request: HttpRequest) => { + return ['Hello ' + text, request instanceof HttpRequest]; + }); + }); + + expect((await httpKernel.request(HttpRequest.GET('/Galaxy'))).json).toEqual(['Hello Galaxy', true]); +}); + +test('router post', async () => { + const httpKernel = createHttpKernel((registry: HttpRouterRegistry) => { + interface User { + id: number; + } + + registry.post('/', (user: HttpBody, request: HttpRequest) => { + return [user, request instanceof HttpRequest]; + }); + }); + + expect((await httpKernel.request(HttpRequest.POST('/').json({ id: 23 }))).json).toEqual([{ id: 23 }, true]); +}); + +test('router options', async () => { + const app = createHttpApp(); + const registry = app.get(HttpRouterRegistry); + + registry.get( + { path: '/user/:id', name: 'user_details' }, + (id: number) => { + return id; + } + ); + + const router = app.get(HttpRouter); + expect(router.resolveUrl('user_details', { id: 12 })).toBe('/user/12'); + + const kernel = app.get(HttpKernel); + + expect((await kernel.request(HttpRequest.GET('/user/23'))).json).toEqual(23); +}); + +test('router decorator options', async () => { + const app = createHttpApp(); + const registry = app.get(HttpRouterRegistry); + + function withError(http: HttpDecorator) { + return http.response(400, 'on error'); + } + + registry.add( + withError(http.GET('/user/:id') + .name('user_details')), + (id: number) => { + return id; + } + ); + + const router = app.get(HttpRouter); + expect(router.resolveUrl('user_details', { id: 12 })).toBe('/user/12'); + + const kernel = app.get(HttpKernel); + + expect((await kernel.request(HttpRequest.GET('/user/23'))).json).toEqual(23); +}); diff --git a/packages/http/tests/router.spec.ts b/packages/http/tests/router.spec.ts index 7751927df..24e747b50 100644 --- a/packages/http/tests/router.spec.ts +++ b/packages/http/tests/router.spec.ts @@ -1,12 +1,14 @@ import { expect, test } from '@jest/globals'; -import { dotToUrlPath, RouteParameterResolverContext, Router, UploadedFile } from '../src/router'; +import { dotToUrlPath, RouteParameterResolverContext, HttpRouter, UploadedFile, RouteClassControllerAction } from '../src/router'; import { http, httpClass } from '../src/decorator'; -import { HttpBadRequestError, httpWorkflow, JSONResponse } from '../src/http'; +import { HttpBadRequestError, httpWorkflow, JSONResponse, Response } from '../src/http'; import { eventDispatcher } from '@deepkit/event'; import { HttpBody, HttpBodyValidation, HttpQueries, HttpQuery, HttpRegExp, HttpRequest } from '../src/model'; import { getClassName, sleep } from '@deepkit/core'; import { createHttpKernel } from './utils'; import { Group, MinLength } from '@deepkit/type'; +import { HttpModule } from '../src/module.js'; +import { HttpKernel } from '../src/kernel.js'; test('router', async () => { class Controller { @@ -31,7 +33,7 @@ test('router', async () => { } } - const router = Router.forControllers([Controller]); + const router = HttpRouter.forControllers([Controller]); expect((await router.resolve('GET', '/'))?.routeConfig.action).toMatchObject({ controller: Controller, methodName: 'helloWorld' }); expect((await router.resolve('GET', '/peter'))?.routeConfig.action).toMatchObject({ controller: Controller, methodName: 'hello' }); @@ -53,11 +55,11 @@ test('any', async () => { } } - const router = Router.forControllers([Controller]); + const router = HttpRouter.forControllers([Controller]); - expect((await router.resolve('GET', '/any'))!.routeConfig.action.methodName).toEqual('any'); - expect((await router.resolve('POST', '/any'))!.routeConfig.action.methodName).toEqual('any'); - expect((await router.resolve('OPTIONS', '/any'))!.routeConfig.action.methodName).toEqual('any'); + expect(((await router.resolve('GET', '/any'))!.routeConfig.action as RouteClassControllerAction).methodName).toEqual('any'); + expect(((await router.resolve('POST', '/any'))!.routeConfig.action as RouteClassControllerAction).methodName).toEqual('any'); + expect(((await router.resolve('OPTIONS', '/any'))!.routeConfig.action as RouteClassControllerAction).methodName).toEqual('any'); }); test('router parameters', async () => { @@ -83,7 +85,7 @@ test('router parameters', async () => { } } - const router = Router.forControllers([Controller]); + const router = HttpRouter.forControllers([Controller]); expect((await router.resolve('GET', '/user/peter'))?.routeConfig.action).toMatchObject({ controller: Controller, methodName: 'string' }); const httpKernel = createHttpKernel([Controller]); @@ -98,6 +100,21 @@ test('router parameters', async () => { expect((await httpKernel.request(HttpRequest.GET('/any/path'))).json).toBe('any/path'); }); +test('generic response', async () => { + class Controller { + @http.GET('xml') + xml() { + return new Response('Hello', 'text/xml'); + } + } + + const httpKernel = createHttpKernel([Controller]); + + const xmlResponse = await httpKernel.request(HttpRequest.GET('/xml')); + expect(xmlResponse.bodyString).toBe('Hello'); + expect(xmlResponse.getHeader('content-type')).toBe('text/xml'); +}); + test('router HttpRequest', async () => { class Controller { @http.GET(':path') @@ -280,7 +297,7 @@ test('router body is safe for simultaneous requests', async () => { expect(results).toEqual( results.map(() => ['Peter', true, '/']), - ) + ); }); test('router body interface', async () => { @@ -457,6 +474,17 @@ test('hook after serializer', async () => { expect(result.processingTime).toBeGreaterThanOrEqual(99); }); +test('functional hooks', async () => { + const httpKernel = createHttpKernel([], [], [ + httpWorkflow.onResponse.listen((event) => { + event.result = { username: 'Peter' }; + }) + ]); + + const result = (await httpKernel.request(HttpRequest.GET('/'))).json; + expect(result).toEqual({ username: 'Peter' }); +}); + test('invalid route definition', async () => { class Controller { @http.GET() @@ -509,6 +537,29 @@ test('inject request storage ClassType', async () => { expect((await httpKernel.request(HttpRequest.GET('/optional').headers({ authorization: 'no' }))).json).toEqual({ isUser: false }); }); +test('functional listener', async () => { + class Controller { + @http.GET('hello') + hello() { + return 'hi'; + } + } + + const gotUrls: string[] = []; + + const httpKernel = createHttpKernel([Controller], [], [ + httpWorkflow.onController.listen(event => { + gotUrls.push(event.request.url || ''); + }) + ]); + + const response = await httpKernel.request(HttpRequest.GET('/hello')); + expect(response.statusCode).toBe(200); + expect(response.json).toBe('hi'); + expect(gotUrls).toEqual(['/hello']); +}); + + test('custom request handling', async () => { class Listener { @eventDispatcher.listen(httpWorkflow.onRouteNotFound) @@ -666,7 +717,7 @@ test('router url resolve', async () => { } } - const router = Router.forControllers([Controller]); + const router = HttpRouter.forControllers([Controller]); expect(router.resolveUrl('first')).toBe('/'); expect(router.resolveUrl('second', { peter: 'foo' })).toBe('/foo'); diff --git a/packages/http/tests/utils.ts b/packages/http/tests/utils.ts index 9f5a66f1f..3e12e1791 100644 --- a/packages/http/tests/utils.ts +++ b/packages/http/tests/utils.ts @@ -1,26 +1,43 @@ -import { ClassType, isClass } from '@deepkit/core'; +import { ClassType, isArray, isClass, isFunction } from '@deepkit/core'; import { ProviderWithScope } from '@deepkit/injector'; import { HttpKernel } from '../src/kernel'; import { App, AppModule, MiddlewareFactory } from '@deepkit/app'; +import { EventListener } from '@deepkit/event'; import { HttpModule } from '../src/module'; +import { HttpRouterRegistry } from '../src/router.js'; export function createHttpKernel( - controllers: (ClassType | { module: AppModule, controller: ClassType })[], + controllers: (ClassType | { module: AppModule, controller: ClassType })[] | ((registry: HttpRouterRegistry) => void) = [], providers: ProviderWithScope[] = [], - listeners: ClassType[] = [], + listeners: (EventListener | ClassType)[] = [], + middlewares: MiddlewareFactory[] = [], + modules: AppModule[] = [] +) { + const app = createHttpApp(controllers, providers, listeners, middlewares, modules); + + return app.get(HttpKernel); +} + +export function createHttpApp( + controllers: (ClassType | { module: AppModule, controller: ClassType })[] | ((registry: HttpRouterRegistry) => void) = [], + providers: ProviderWithScope[] = [], + listeners: (EventListener | ClassType)[] = [], middlewares: MiddlewareFactory[] = [], modules: AppModule[] = [] ) { const imports: AppModule[] = modules.slice(0); imports.push(new HttpModule()); - for (const controller of controllers) { - if (isClass(controller)) continue; - imports.push(controller.module); + if (isArray(controllers)) { + for (const controller of controllers) { + if (isClass(controller)) continue; + if (isFunction(controller)) continue; + imports.push(controller.module); + } } const module = new AppModule({ - controllers: controllers.map(v => isClass(v) ? v : v.controller), + controllers: isArray(controllers) ? controllers.map(v => isClass(v) ? v : v.controller) : [], imports, providers, listeners, @@ -28,5 +45,11 @@ export function createHttpKernel( }); const app = App.fromModule(module); - return app.get(HttpKernel); + + if (!isArray(controllers)) { + const registry = app.get(HttpRouterRegistry); + controllers(registry); + } + + return app; } diff --git a/packages/injector/index.ts b/packages/injector/index.ts index 484219ee6..1d1022ab7 100644 --- a/packages/injector/index.ts +++ b/packages/injector/index.ts @@ -1,4 +1,4 @@ -export * from './src/injector'; -export * from './src/provider'; -export * from './src/module'; -export * from './src/types'; +export * from './src/injector.js'; +export * from './src/provider.js'; +export * from './src/module.js'; +export * from './src/types.js'; diff --git a/packages/injector/package.json b/packages/injector/package.json index 0d45a7efa..6d21ab55c 100644 --- a/packages/injector/package.json +++ b/packages/injector/package.json @@ -35,6 +35,9 @@ "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "testMatch": [ "**/tests/**/*.spec.ts" ] diff --git a/packages/injector/src/injector.ts b/packages/injector/src/injector.ts index 64283d52e..d75b8e0d3 100644 --- a/packages/injector/src/injector.ts +++ b/packages/injector/src/injector.ts @@ -1,6 +1,17 @@ -import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, NormalizedProvider, ProviderWithScope, Tag, TagProvider, TagRegistry, Token } from './provider'; +import { + isClassProvider, + isExistingProvider, + isFactoryProvider, + isValueProvider, + NormalizedProvider, + ProviderWithScope, + Tag, + TagProvider, + TagRegistry, + Token +} from './provider.js'; import { AbstractClassType, ClassType, CompilerContext, CustomError, getClassName, isArray, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core'; -import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module'; +import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module.js'; import { hasTypeInformation, isExtendable, @@ -376,7 +387,7 @@ export class Injector implements InjectorInterface { name: parameter.name, type: tokenType || parameter.getType() as Type, optional: !parameter.isValueRequired() - }, provider, compiler, resolveDependenciesFrom, reflection.name || 'useFactory', args.length, 'constructorParameterNotFound')); + }, provider, compiler, resolveDependenciesFrom, reflection.name || 'useFactory', args.length, 'factoryDependencyNotFound')); } factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; @@ -713,7 +724,7 @@ export class Injector implements InjectorInterface { } } - if (isWithAnnotations(type) && type.indexAccessOrigin) { + if (type.indexAccessOrigin) { let current = type; let module: InjectorModule | undefined; let config: { [name: string]: any } = {}; @@ -725,8 +736,8 @@ export class Injector implements InjectorInterface { config = module.getConfig(); } if (current.indexAccessOrigin.index.kind === ReflectionKind.literal) { - const index = JSON.stringify(current.indexAccessOrigin.index.literal); - if (config) config = config[index]; + const index = current.indexAccessOrigin.index.literal; + config = config[String(index)]; } current = current.indexAccessOrigin.container; } @@ -766,18 +777,16 @@ export class Injector implements InjectorInterface { ); } - const allPossibleScopes = foundPreparedProvider.providers.map(getScope); - const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; - - if (!unscoped && !allPossibleScopes.includes(fromScope)) { - const t = stringifyType(type, { showFullDefinition: false }); - throw new ServiceNotFoundError( - `Service "${t}" can not be received from ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + - `since it only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.` - ); - } - - const foundToken = foundPreparedProvider.token; + // const allPossibleScopes = foundPreparedProvider.providers.map(getScope); + // const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; + // + // if (!unscoped && !allPossibleScopes.includes(fromScope)) { + // const t = stringifyType(type, { showFullDefinition: false }); + // throw new ServiceNotFoundError( + // `Service "${t}" can not be received from ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + + // `since it only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.` + // ); + // } const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; @@ -867,3 +876,39 @@ export class InjectorContext { return new InjectorContext(this.rootModule, { name: scope, instances: {} }, this.buildContext); } } + +export function injectedFunction any>(fn: T, injector: Injector, skipParameters: number = 0): ((scope?: Scope, ...args: any[]) => ReturnType) { + const type = reflect(fn); + if (type.kind === ReflectionKind.function) { + const args: Resolver[] = []; + for (let i = skipParameters; i < type.parameters.length; i++) { + args.push(injector.createResolver(type.parameters[i])); + } + + if (skipParameters === 0) { + return ((scope: Scope | undefined) => { + return fn(...(args.map(v => v(scope)))); + }) as any; + } else if (skipParameters === 1) { + return ((scope: Scope | undefined, p1: any) => { + return fn(p1, ...(args.map(v => v(scope)))); + }) as any; + } else if (skipParameters === 2) { + return ((scope: Scope | undefined, p1: any, p2: any) => { + return fn(p1, p2, ...(args.map(v => v(scope)))); + }) as any; + } else if (skipParameters === 3) { + return ((scope: Scope | undefined, p1: any, p2: any, p3: any) => { + return fn(p1, p2, p3, ...(args.map(v => v(scope)))); + }) as any; + } else { + return ((scope: Scope | undefined, ...input: any[]) => { + while (input.length !== skipParameters) { + input.push(undefined); + } + return fn(...input.slice(0, skipParameters), ...(args.map(v => v(scope)))); + }) as any; + } + } + return fn; +} diff --git a/packages/injector/src/module.ts b/packages/injector/src/module.ts index 83b7710d2..f1fd3f953 100644 --- a/packages/injector/src/module.ts +++ b/packages/injector/src/module.ts @@ -1,6 +1,6 @@ -import { NormalizedProvider, ProviderWithScope, TagProvider, Token } from './provider'; -import { arrayRemoveItem, ClassType, getClassName, isClass, isPrototypeOfBase } from '@deepkit/core'; -import { BuildContext, Injector, SetupProviderRegistry } from './injector'; +import { NormalizedProvider, ProviderWithScope, TagProvider, Token } from './provider.js'; +import { arrayRemoveItem, ClassType, getClassName, isClass, isPlainObject, isPrototypeOfBase } from '@deepkit/core'; +import { BuildContext, Injector, SetupProviderRegistry } from './injector.js'; import { hasTypeInformation, isExtendable, isType, ReceiveType, reflect, ReflectionKind, resolveReceiveType } from '@deepkit/type'; export type ConfigureProvider = { [name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider : T[name] }; @@ -74,6 +74,9 @@ function lookupPreparedProviders(preparedProviders: PreparedProvider[], token: T function registerPreparedProvider(preparedProviders: PreparedProvider[], modules: InjectorModule[], providers: NormalizedProvider[], replaceExistingScope: boolean = true) { const token = providers[0].provide; + if (token === undefined) { + throw new Error('token is undefined: ' + JSON.stringify(providers)); + } const preparedProvider = lookupPreparedProviders(preparedProviders, token); if (preparedProvider) { preparedProvider.token = token; @@ -167,7 +170,7 @@ export class InjectorModule { expect(c).toBeInstanceOf(B); expect(c.a).toBeInstanceOf(A); }); + +test('injectedFunction all', () => { + class A {} + class B {} + const injector = Injector.from([A, B]); + + function render(a: A, b: B) { + expect(a).toBeInstanceOf(A); + expect(b).toBeInstanceOf(B); + return true; + } + + const wrapped = injectedFunction(render, injector); + + expect(wrapped()).toBe(true); +}); + +test('injectedFunction scope', () => { + class A {} + class B { + constructor(public id: number) { + } + } + const injector = InjectorContext.forProviders([A, {provide: B, scope: 'http', useValue: new B(0)}]); + + function render(a: A, b: B) { + expect(a).toBeInstanceOf(A); + expect(b).toBeInstanceOf(B); + return b.id; + } + + const wrapped = injectedFunction(render, injector.getRootInjector()); + + { + const scope = injector.createChildScope('http'); + expect(wrapped(scope.scope)).toBe(0); + } + { + const scope = injector.createChildScope('http'); + scope.set(B, new B(1)); + expect(wrapped(scope.scope)).toBe(1); + } + { + const scope = injector.createChildScope('http'); + scope.set(B, new B(2)); + expect(wrapped(scope.scope)).toBe(2); + } +}); + +test('injectedFunction skip 1', () => { + class A {} + class B {} + const injector = Injector.from([A, B]); + + function render(html: string, a: A, b: B) { + expect(a).toBeInstanceOf(A); + expect(b).toBeInstanceOf(B); + return html; + } + + const wrapped = injectedFunction(render, injector, 1); + + expect(wrapped(undefined, 'abc')).toBe('abc'); +}); + +test('injectedFunction skip 2', () => { + class A {} + class B {} + const injector = Injector.from([A, B]); + + function render(html: string, a: A, b: B) { + expect(a).toBeInstanceOf(A); + expect(b).toBeInstanceOf(B); + return html; + } + + const wrapped = injectedFunction(render, injector, 2); + + expect(wrapped(undefined, 'abc', new A)).toBe('abc'); +}); diff --git a/packages/injector/tests/injector2.spec.ts b/packages/injector/tests/injector2.spec.ts index 5bb02301d..507400937 100644 --- a/packages/injector/tests/injector2.spec.ts +++ b/packages/injector/tests/injector2.spec.ts @@ -27,7 +27,7 @@ test('parent dependency', () => { } const module1 = new InjectorModule([Router]); - const module2 = new InjectorModule([Controller], module1); + const module2 = new InjectorModule([Controller], module1) const context = new InjectorContext(module1); const injector = context.getInjector(module2); @@ -666,7 +666,6 @@ test('setup provider by interface 2', () => { expect(service.list).toEqual(['a', 'b']); }); - test('setup provider in sub module', () => { class Service { list: any[] = []; @@ -860,6 +859,7 @@ test('provide() with provider', () => { return true; } } + class Service { constructor(public redis: Redis) { } @@ -1395,14 +1395,16 @@ test('inject via symbols', () => { }); test('class inheritance', () => { - class A {} + class A { + } class B { constructor(public a: A) { } } - class C extends B {} + class C extends B { + } const app = new InjectorModule([A, C]); const injector = new InjectorContext(app); diff --git a/packages/mongo/package-lock.json b/packages/mongo/package-lock.json index bc90fbf81..99ba73a6d 100644 --- a/packages/mongo/package-lock.json +++ b/packages/mongo/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@deepkit/mongo", - "version": "1.0.1-alpha.70", + "version": "1.0.1-alpha.71", "license": "MIT", "dependencies": { "saslprep": "^1.0.3", diff --git a/packages/orm/package.json b/packages/orm/package.json index 3e601634a..81fb68a5b 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -49,6 +49,9 @@ "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "testMatch": [ "**/tests/**/*.spec.ts" ] diff --git a/packages/orm/src/database-adapter.ts b/packages/orm/src/database-adapter.ts index 2e766d52b..f13c924a0 100644 --- a/packages/orm/src/database-adapter.ts +++ b/packages/orm/src/database-adapter.ts @@ -18,7 +18,7 @@ export abstract class DatabaseAdapterQueryFactory { abstract createQuery(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass): Query; } -export interface DatabasePersistenceChangeSet { +export interface DatabasePersistenceChangeSet { changes: ItemChanges; item: T; primaryKey: PrimaryKeyFields; diff --git a/packages/orm/src/database-session.ts b/packages/orm/src/database-session.ts index 995e2ce94..7b1571024 100644 --- a/packages/orm/src/database-session.ts +++ b/packages/orm/src/database-session.ts @@ -12,7 +12,17 @@ import type { DatabaseAdapter, DatabasePersistence, DatabasePersistenceChangeSet import { DatabaseEntityRegistry } from './database-adapter'; import { DatabaseValidationError, OrmEntity } from './type'; import { ClassType, CustomError } from '@deepkit/core'; -import { getPrimaryKeyExtractor, isReferenceInstance, markAsHydrated, PrimaryKeyFields, ReflectionClass, typeSettings, UnpopulatedCheck, validate } from '@deepkit/type'; +import { + ReceiveType, + getPrimaryKeyExtractor, + isReferenceInstance, + markAsHydrated, + PrimaryKeyFields, + ReflectionClass, + typeSettings, + UnpopulatedCheck, + validate +} from '@deepkit/type'; import { GroupArraySort } from '@deepkit/topsort'; import { getClassState, getInstanceState, getNormalizedPrimaryKey, IdentityMap } from './identity-map'; import { getClassSchemaInstancePairs } from './utils'; @@ -21,6 +31,7 @@ import { getReference } from './reference'; import { QueryDatabaseEmitter, UnitOfWorkCommitEvent, UnitOfWorkDatabaseEmitter, UnitOfWorkEvent, UnitOfWorkUpdateEvent } from './event'; import { DatabaseLogger } from './logger'; import { Stopwatch } from '@deepkit/stopwatch'; +import { AbstractClassType } from '@deepkit/core'; let SESSION_IDS = 0; @@ -65,7 +76,7 @@ export class DatabaseSessionRound { protected getReferenceDependencies(item: T): OrmEntity[] { const result: OrmEntity[] = []; - const classSchema = this.session.entityRegistry.getFromInstance(item) + const classSchema = this.session.entityRegistry.getFromInstance(item); const old = typeSettings.unpopulatedCheck; typeSettings.unpopulatedCheck = UnpopulatedCheck.None; @@ -290,7 +301,14 @@ export class DatabaseSession { public stopwatch?: Stopwatch, ) { const queryFactory = this.adapter.queryFactory(this); - this.query = queryFactory.createQuery.bind(queryFactory); + + const self = this; + + //we cannot use arrow functions, since they can't have ReceiveType + function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { + return queryFactory.createQuery(type); + }; + this.query = query as any; const factory = this.adapter.rawFactory(this); this.raw = factory.create.bind(factory); diff --git a/packages/orm/src/database.ts b/packages/orm/src/database.ts index 05fff3614..85a62ebf6 100644 --- a/packages/orm/src/database.ts +++ b/packages/orm/src/database.ts @@ -130,11 +130,14 @@ export class Database { this.entityRegistry.add(...schemas); if (Database.registry) Database.registry.push(this); - this.query = (classType: ClassType | ReflectionClass) => { - const session = this.createSession(); + const self = this; + //we cannot use arrow functions, since they can't have ReceiveType + function query(type?: ReceiveType | ClassType | AbstractClassType | ReflectionClass) { + const session = self.createSession(); session.withIdentityMap = false; - return session.query(classType); + return session.query(type); }; + this.query = query; this.raw = (...args: any[]) => { const session = this.createSession(); @@ -163,12 +166,14 @@ export class Database { } static createClass(name: string, adapter: T, schemas: (ClassType | ReflectionClass)[] = []): ClassType> { - return class extends Database { - constructor(oAdapter = adapter, oSchemas = schemas) { - super(oAdapter, oSchemas); + class C extends Database { + bla!: string; + constructor() { + super(adapter, schemas); this.name = name; } - }; + } + return C; } /** diff --git a/packages/orm/src/event.ts b/packages/orm/src/event.ts index a3b9857de..c057a540a 100644 --- a/packages/orm/src/event.ts +++ b/packages/orm/src/event.ts @@ -47,7 +47,7 @@ export class UnitOfWorkEvent extends AsyncEmitterEvent { } } -export class UnitOfWorkUpdateEvent extends AsyncEmitterEvent { +export class UnitOfWorkUpdateEvent extends AsyncEmitterEvent { constructor( public readonly classSchema: ReflectionClass, public readonly databaseSession: DatabaseSession, @@ -56,7 +56,7 @@ export class UnitOfWorkUpdateEvent extends AsyncEmitterEvent { super(); } - isSchemaOf(classType: ClassType): this is UnitOfWorkUpdateEvent { + isSchemaOf(classType: ClassType): this is UnitOfWorkUpdateEvent { return this.classSchema.isSchemaOf(classType); } } @@ -110,7 +110,7 @@ export class QueryDatabaseDeleteEvent extends AsyncEmitterEvent { } } -export class QueryDatabasePatchEvent extends AsyncEmitterEvent { +export class QueryDatabasePatchEvent extends AsyncEmitterEvent { public returning: (keyof T & string)[] = []; constructor( @@ -123,7 +123,7 @@ export class QueryDatabasePatchEvent extends AsyncEmitterEvent { super(); } - isSchemaOf(classType: ClassType): this is QueryDatabasePatchEvent { + isSchemaOf(classType: ClassType): this is QueryDatabasePatchEvent { return this.classSchema.isSchemaOf(classType); } } diff --git a/packages/orm/src/query.ts b/packages/orm/src/query.ts index 6934d03cf..90cf4ba6d 100644 --- a/packages/orm/src/query.ts +++ b/packages/orm/src/query.ts @@ -476,7 +476,7 @@ export class BaseQuery { } } -export abstract class GenericQueryResolver = DatabaseQueryModel> { +export abstract class GenericQueryResolver = DatabaseQueryModel> { constructor( protected classSchema: ReflectionClass, protected session: DatabaseSession, diff --git a/packages/orm/src/utils.ts b/packages/orm/src/utils.ts index 6842ee353..1c9ac54ab 100644 --- a/packages/orm/src/utils.ts +++ b/packages/orm/src/utils.ts @@ -49,7 +49,7 @@ export type Placeholder = () => T; export type Resolve }> = ReturnType; export type Replace = T & { _: Placeholder }; -export function buildChangesFromInstance(item: T): Changes { +export function buildChangesFromInstance(item: T): Changes { const state = getInstanceStateFromItem(item); const lastSnapshot = state.getSnapshot(); const currentSnapshot = state.classState.snapshot(item); diff --git a/packages/rpc-tcp/package-lock.json b/packages/rpc-tcp/package-lock.json index b2bd75002..c7991d219 100644 --- a/packages/rpc-tcp/package-lock.json +++ b/packages/rpc-tcp/package-lock.json @@ -6,10 +6,12 @@ "packages": { "": { "name": "@deepkit/rpc-tcp", - "version": "1.0.1-alpha.63", + "version": "1.0.1-alpha.71", "license": "MIT", "dependencies": { - "turbo-net": "deepkit/turbo-net#1.6.0" + "@types/ws": "^8.5.3", + "turbo-net": "deepkit/turbo-net#1.6.0", + "ws": "^8.6.0" }, "peerDependencies": { "@deepkit/core": "^1.0.1-alpha.13", @@ -121,6 +123,11 @@ "integrity": "sha512-9SrLCpgzWo2yHHhiMOX0WwgDh37nSbDbWUsRc1ss++o8O97E3tB6SJiyUQM21UeUsKvZNuhDCmkRaINZ4uJAfg==", "peer": true }, + "node_modules/@types/node": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", + "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -133,6 +140,14 @@ "integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==", "peer": true }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -342,6 +357,26 @@ "bin": { "uuid": "dist/bin/uuid" } + }, + "node_modules/ws": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } }, "dependencies": { @@ -421,6 +456,11 @@ "integrity": "sha512-9SrLCpgzWo2yHHhiMOX0WwgDh37nSbDbWUsRc1ss++o8O97E3tB6SJiyUQM21UeUsKvZNuhDCmkRaINZ4uJAfg==", "peer": true }, + "@types/node": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", + "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==" + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -433,6 +473,14 @@ "integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==", "peer": true }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -563,6 +611,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "peer": true + }, + "ws": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "requires": {} } } } diff --git a/packages/rpc-tcp/package.json b/packages/rpc-tcp/package.json index fb047fc4b..d71f17c27 100644 --- a/packages/rpc-tcp/package.json +++ b/packages/rpc-tcp/package.json @@ -25,7 +25,9 @@ "@deepkit/rpc": "^1.0.1-alpha.13" }, "dependencies": { - "turbo-net": "deepkit/turbo-net#1.6.0" + "turbo-net": "deepkit/turbo-net#1.6.0", + "@types/ws": "^8.5.3", + "ws": "^8.6.0" }, "devDependencies": { "@deepkit/rpc": "^1.0.1-alpha.71" diff --git a/packages/rpc-tcp/src/client.ts b/packages/rpc-tcp/src/client.ts index 882b3557f..3761aae87 100644 --- a/packages/rpc-tcp/src/client.ts +++ b/packages/rpc-tcp/src/client.ts @@ -8,7 +8,7 @@ import * as turbo from 'turbo-net'; /** * Uses `turbo-net` module to connect to the server. */ -export class TcpRpcClientAdapter implements ClientTransportAdapter { +export class RpcTcpClientAdapter implements ClientTransportAdapter { protected host: ParsedHost; public bufferSize: number = 100 * 1024 //100kb per connection; @@ -72,7 +72,7 @@ export class TcpRpcClientAdapter implements ClientTransportAdapter { /* * Uses the node `net` module to connect. Supports unix sockets. */ -export class NetTcpRpcClientAdapter implements ClientTransportAdapter { +export class RpcNetTcpClientAdapter implements ClientTransportAdapter { protected host; constructor( diff --git a/packages/rpc-tcp/src/server.ts b/packages/rpc-tcp/src/server.ts index fad441ad4..9ef294d46 100644 --- a/packages/rpc-tcp/src/server.ts +++ b/packages/rpc-tcp/src/server.ts @@ -8,11 +8,11 @@ import { createServer, Server, Socket } from 'net'; /** * Uses the `turbo-net` module to create a server. */ -export class TcpRpcServer { +export class RpcTcpServer { protected turbo?: any; protected host: ParsedHost; - public bufferSize: number = 25 * 1024 //25kb per connection; + public bufferSize: number = 25 * 1024; //25kb per connection; constructor( protected kernel: RpcKernel, @@ -88,7 +88,7 @@ export class TcpRpcServer { /** * Uses the node `net` module to create a server. Supports unix sockets. */ -export class NetTcpRpcServer { +export class RpcNetTcpServer { protected server?: Server; protected host: ParsedHost; @@ -155,3 +155,56 @@ export class NetTcpRpcServer { this.server?.close(); } } + +import ws from 'ws'; +import type { ServerOptions as WebSocketServerOptions } from 'ws'; +import { IncomingMessage } from 'http'; + +export class RpcWebSocketServer { + protected server?: ws.Server; + protected host: ParsedHost; + + constructor( + protected kernel: RpcKernel, + host: string + ) { + this.host = parseHost(host); + if (this.host.isUnixSocket && existsSync(this.host.unixSocket)) { + unlinkSync(this.host.unixSocket); + } + } + + close() { + this.server?.close(); + } + + start(options: WebSocketServerOptions): void { + const defaultOptions = { host: this.host.host, port: this.host.port }; + this.server = new ws.Server({ ...defaultOptions, ...options }); + + this.server.on('connection', (ws, req: IncomingMessage) => { + const connection = this.kernel?.createConnection({ + write(b) { + ws.send(b); + }, + close() { + ws.close(); + }, + bufferedAmount(): number { + return ws.bufferedAmount; + }, + clientAddress(): string { + return req.socket.remoteAddress || ''; + } + }); + + ws.on('message', async (message: Uint8Array) => { + connection.feed(message); + }); + + ws.on('close', async () => { + connection.close(); + }); + }); + } +} diff --git a/packages/rpc/src/client/client-websocket.ts b/packages/rpc/src/client/client-websocket.ts index 0caabc360..780a9fcbd 100644 --- a/packages/rpc/src/client/client-websocket.ts +++ b/packages/rpc/src/client/client-websocket.ts @@ -39,7 +39,7 @@ export class RpcWebSocketClientAdapter implements ClientTransportAdapter { public async connect(connection: TransportConnectionHooks) { const wsPackage = 'ws'; - const webSocketConstructor = 'undefined' === typeof WebSocket && require ? require(wsPackage) : WebSocket; + const webSocketConstructor = 'undefined' === typeof WebSocket && 'undefined' !== typeof require ? require(wsPackage) : WebSocket; const socket = new webSocketConstructor(this.url); socket.binaryType = 'arraybuffer'; diff --git a/packages/rpc/src/client/client.ts b/packages/rpc/src/client/client.ts index 9df706604..34f868cdb 100644 --- a/packages/rpc/src/client/client.ts +++ b/packages/rpc/src/client/client.ts @@ -615,18 +615,18 @@ export class RpcClient extends RpcBaseClient { * Registers a new controller for the peer's RPC kernel. * Use `registerAsPeer` first. */ - public registerPeerController(nameOrDefinition: string | ControllerDefinition, classType: ClassType) { + public registerPeerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { if (!this.peerKernel) throw new Error('Not registered as peer. Call registerAsPeer() first'); - this.peerKernel.registerController('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path, classType); + this.peerKernel.registerController(classType, nameOrDefinition); } /** * Registers a new controller for the server's RPC kernel. * This is when the server wants to communicate actively with the client (us). */ - public registerController(nameOrDefinition: string | ControllerDefinition, classType: ClassType) { + public registerController(classType: ClassType, nameOrDefinition: string | ControllerDefinition) { if (!this.clientKernel) this.clientKernel = new RpcKernel(); - this.clientKernel.registerController('string' === typeof nameOrDefinition ? nameOrDefinition : nameOrDefinition.path, classType); + this.clientKernel.registerController(classType, nameOrDefinition); } public async registerAsPeer(id: string) { diff --git a/packages/rpc/src/client/message-subject.ts b/packages/rpc/src/client/message-subject.ts index 213d758d7..0564f5e70 100644 --- a/packages/rpc/src/client/message-subject.ts +++ b/packages/rpc/src/client/message-subject.ts @@ -109,13 +109,17 @@ export class RpcMessageSubject { } async firstThenClose(type: number, schema?: ReceiveType): Promise { - return asyncOperation((resolve, reject) => { + return await asyncOperation((resolve, reject) => { this.onReply((next) => { this.onReplyCallback = this.catchOnReplyCallback; this.release(); if (next.type === type) { - return resolve(schema ? next.parseBody(schema) : next); + try { + return resolve(schema ? next.parseBody(schema) : next); + } catch (error: any) { + return reject(error); + } } if (next.isError()) { diff --git a/packages/rpc/src/protocol.ts b/packages/rpc/src/protocol.ts index d3d191df6..1e6bf98f5 100644 --- a/packages/rpc/src/protocol.ts +++ b/packages/rpc/src/protocol.ts @@ -151,7 +151,9 @@ export class RpcMessage { } parseBody(type?: ReceiveType): T { - if (!this.bodySize) throw new Error('Message has no body'); + if (!this.bodySize) { + throw new Error('Message has no body'); + } if (!this.buffer) throw new Error('No buffer'); if (this.composite) throw new Error('Composite message can not be read directly'); // console.log('parseBody raw', deserializeBSONWithoutOptimiser(this.buffer, this.bodyOffset)); diff --git a/packages/rpc/src/server/action.ts b/packages/rpc/src/server/action.ts index c65a25d6e..6e7a5bfa0 100644 --- a/packages/rpc/src/server/action.ts +++ b/packages/rpc/src/server/action.ts @@ -163,7 +163,7 @@ export class RpcServerAction { } const methodReflection = ReflectionClass.from(classType.controller).getMethod(methodName); - const method = methodReflection.method; + const method = methodReflection.type; assertType(method, ReflectionKind.method); let mode: ActionMode = 'arbitrary'; diff --git a/packages/rpc/src/server/kernel.ts b/packages/rpc/src/server/kernel.ts index 662cf0377..57e84e527 100644 --- a/packages/rpc/src/server/kernel.ts +++ b/packages/rpc/src/server/kernel.ts @@ -33,6 +33,8 @@ import { RpcActionClient, RpcControllerState } from '../client/action'; import { RemoteController } from '../client/client'; import { InjectorContext, InjectorModule } from '@deepkit/injector'; import { Logger, LoggerInterface } from '@deepkit/logger'; +import { rpcClass } from '../decorators'; +import { getClassName } from '@deepkit/core'; export class RpcCompositeMessage { protected messages: RpcCreateMessageDef[] = []; @@ -470,17 +472,21 @@ export class RpcKernel { } /** - * This registers the controller and adds it as provider to the injector. + * This registers the controller and no custom InjectorContext was given adds it as provider to the injector. * - * If you created a kernel with custom injector, you probably want to set addAsProvider to false. - * Adding a provider is rather expensive, so you should prefer to create a kernel with pre-filled injector. + * Note: Controllers can not be added to the injector when the injector was already built. */ - public registerController(id: string | ControllerDefinition, controller: ClassType, module?: InjectorModule) { + public registerController(controller: ClassType, id?: string | ControllerDefinition, module?: InjectorModule) { if (this.autoInjector) { if (!this.injector.rootModule.isProvided(controller)) { this.injector.rootModule.addProvider({ provide: controller, scope: 'rpc' }); } } + if (!id) { + const rpcConfig = rpcClass._fetch(controller); + if (!rpcConfig) throw new Error(`Controller ${getClassName(controller)} has no @rpc.controller() decorator and no controller id was provided.`); + id = rpcConfig.getPath(); + } this.controllers.set('string' === typeof id ? id : id.path, { controller, module: module || this.injector.rootModule }); } diff --git a/packages/rpc/tests/back-controller.spec.ts b/packages/rpc/tests/back-controller.spec.ts index 240e43384..25339b186 100644 --- a/packages/rpc/tests/back-controller.spec.ts +++ b/packages/rpc/tests/back-controller.spec.ts @@ -21,12 +21,12 @@ test('back controller', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); - client.registerController('myController', Controller); + client.registerController(Controller, 'myController'); expect(await controller.foo('1')).toBe('1'); expect(await controller.triggerClientCall()).toBe('2'); diff --git a/packages/rpc/tests/case/crud.spec.ts b/packages/rpc/tests/case/crud.spec.ts index 1891e23f3..5c0efdf6d 100644 --- a/packages/rpc/tests/case/crud.spec.ts +++ b/packages/rpc/tests/case/crud.spec.ts @@ -21,7 +21,7 @@ test('partial', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); diff --git a/packages/rpc/tests/chunks.spec.ts b/packages/rpc/tests/chunks.spec.ts index c6996ec74..017a3574c 100644 --- a/packages/rpc/tests/chunks.spec.ts +++ b/packages/rpc/tests/chunks.spec.ts @@ -20,7 +20,7 @@ test('chunks', async () => { } const kernel = new RpcKernel(); - kernel.registerController('test', TestController); + kernel.registerController(TestController, 'test'); const client = new DirectClient(kernel); const controller = client.controller('test'); diff --git a/packages/rpc/tests/collection.spec.ts b/packages/rpc/tests/collection.spec.ts index 412bf5665..ca1ae6d65 100644 --- a/packages/rpc/tests/collection.spec.ts +++ b/packages/rpc/tests/collection.spec.ts @@ -108,7 +108,7 @@ test('collection state', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); diff --git a/packages/rpc/tests/controller.spec.ts b/packages/rpc/tests/controller.spec.ts index 209e94c33..e1051fe74 100644 --- a/packages/rpc/tests/controller.spec.ts +++ b/packages/rpc/tests/controller.spec.ts @@ -113,6 +113,7 @@ test('basics', async () => { } } + @rpc.controller('myController') class Controller { @rpc.action() createModel(value: string): MyModel { @@ -146,7 +147,7 @@ test('basics', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -193,7 +194,7 @@ test('parameters', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -239,7 +240,7 @@ test('promise', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -286,7 +287,7 @@ test('wrong arguments', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -327,7 +328,7 @@ test('di', async () => { } const kernel = new RpcKernel(); - kernel.registerController('test', Controller); + kernel.registerController(Controller, 'test'); const client = new DirectClient(kernel); const controller = client.controller('test'); @@ -352,7 +353,7 @@ test('connect disconnect', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -398,7 +399,7 @@ test('types', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -435,7 +436,7 @@ test('disable type reuse', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); client.disableTypeReuse(); @@ -444,13 +445,13 @@ test('disable type reuse', async () => { { const res = await controller.test(); expect(res).not.toBeInstanceOf(Model); - expect(res).toEqual({title: '123'}); + expect(res).toEqual({ title: '123' }); expect(getClassName(res)).toBe('Model'); } { const res = await controller.testDeep(); expect(res.items[0]).not.toBeInstanceOf(Model); - expect(res.items[0]).toEqual({title: '123'}); + expect(res.items[0]).toEqual({ title: '123' }); } }); diff --git a/packages/rpc/tests/entity-state.spec.ts b/packages/rpc/tests/entity-state.spec.ts index a848a3b63..9476c8d05 100644 --- a/packages/rpc/tests/entity-state.spec.ts +++ b/packages/rpc/tests/entity-state.spec.ts @@ -104,7 +104,7 @@ test('controller', async () => { { provide: RpcKernelConnection, scope: 'rpc', useValue: undefined }, { provide: Controller, scope: 'rpc' }, ])); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); diff --git a/packages/rpc/tests/observable.spec.ts b/packages/rpc/tests/observable.spec.ts index 054e3571a..531936c91 100644 --- a/packages/rpc/tests/observable.spec.ts +++ b/packages/rpc/tests/observable.spec.ts @@ -42,7 +42,7 @@ test('observable basics', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -100,7 +100,7 @@ test('Subject', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -141,7 +141,7 @@ test('promise Subject', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -176,7 +176,7 @@ test('subject completes automatically when connection closes', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -221,7 +221,7 @@ test('subject redirect of global subject', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -286,7 +286,7 @@ test('observable unsubscribes automatically when connection closes', async () => } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -350,7 +350,7 @@ test('observable different next type', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -399,7 +399,7 @@ test('Behavior', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -472,7 +472,7 @@ test('observable complete', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); diff --git a/packages/rpc/tests/rpc.spec.ts b/packages/rpc/tests/rpc.spec.ts index fe4618666..277347f13 100644 --- a/packages/rpc/tests/rpc.spec.ts +++ b/packages/rpc/tests/rpc.spec.ts @@ -222,7 +222,7 @@ test('rpc kernel', async () => { } const kernel = new RpcKernel(); - kernel.registerController('myController', Controller); + kernel.registerController(Controller, 'myController'); const client = new DirectClient(kernel); const controller = client.controller('myController'); @@ -248,7 +248,7 @@ test('rpc peer', async () => { } await client1.registerAsPeer('peer1'); - client1.registerPeerController('foo', Controller); + client1.registerPeerController(Controller, 'foo'); const client2 = new DirectClient(kernel); diff --git a/packages/rpc/tests/security.spec.ts b/packages/rpc/tests/security.spec.ts index afe2db2ef..d33ce9a63 100644 --- a/packages/rpc/tests/security.spec.ts +++ b/packages/rpc/tests/security.spec.ts @@ -35,7 +35,7 @@ test('authentication', async () => { } const kernel = new RpcKernel(undefined, new MyKernelSecurity); - kernel.registerController('test', Controller); + kernel.registerController(Controller, 'test'); { const client = new DirectClient(kernel); @@ -58,7 +58,7 @@ test('authentication', async () => { const client2 = new DirectClient(kernel); client2.token.set('secret'); await client2.registerAsPeer('asd'); - client2.registerPeerController('controller', Controller); + client2.registerPeerController(Controller, 'controller'); expect(await client.peer('asd').controller('controller').test('foo')).toBe('foo'); expect(client.username).toBe('user'); @@ -125,7 +125,7 @@ test('onAuthenticate controllers', async () => { } const kernel = new RpcKernel(undefined); - kernel.registerController('test', Controller); + kernel.registerController(Controller, 'test'); class CustomAuthClient extends AsyncDirectClient { authCalled: number = 0; @@ -210,7 +210,7 @@ test('transformError', async () => { } const kernel = new RpcKernel(undefined, new MyKernelSecurity); - kernel.registerController('test', Controller); + kernel.registerController(Controller, 'test'); const client = new DirectClient(kernel); const controller = client.controller('test'); diff --git a/packages/sql/src/cli/base-command.ts b/packages/sql/src/cli/base-command.ts index f8d1c5842..9cc327560 100644 --- a/packages/sql/src/cli/base-command.ts +++ b/packages/sql/src/cli/base-command.ts @@ -4,7 +4,7 @@ export class BaseCommand { /** * @description Database typescript files to import and read Database information */ - @flag.multiple + @flag path: string[] = []; /** diff --git a/packages/sql/src/sql-adapter.ts b/packages/sql/src/sql-adapter.ts index e586511cb..ad8b716c8 100644 --- a/packages/sql/src/sql-adapter.ts +++ b/packages/sql/src/sql-adapter.ts @@ -73,7 +73,7 @@ export abstract class SQLConnection { constructor( protected connectionPool: SQLConnectionPool, - protected logger: DatabaseLogger = new DatabaseLogger, + public logger: DatabaseLogger = new DatabaseLogger, public transaction?: DatabaseTransaction, public stopwatch?: Stopwatch ) { diff --git a/packages/sqlite/src/sqlite-adapter.ts b/packages/sqlite/src/sqlite-adapter.ts index 2bdd5e288..e2611a7b8 100644 --- a/packages/sqlite/src/sqlite-adapter.ts +++ b/packages/sqlite/src/sqlite-adapter.ts @@ -213,6 +213,9 @@ export class SQLiteConnectionPool extends SQLConnectionPool { connection.released = false; connection.stopwatch = stopwatch; + //first connection is always reused, so we update the logger + if (logger) connection.logger = logger; + this.activeConnections++; if (transaction) { diff --git a/packages/template/src/template.ts b/packages/template/src/template.ts index 54d7dd66c..09c601c99 100644 --- a/packages/template/src/template.ts +++ b/packages/template/src/template.ts @@ -9,7 +9,7 @@ */ import { ClassType, getClassName, isArray, isClass } from '@deepkit/core'; import './optimize-tsx'; -import { Injector, Resolver } from '@deepkit/injector'; +import { injectedFunction, Injector, Resolver } from '@deepkit/injector'; import { FrameCategory, Stopwatch } from '@deepkit/stopwatch'; import { escapeAttribute, escapeHtml, safeString } from './utils'; import { reflect, ReflectionClass, ReflectionKind, Type } from '@deepkit/type'; @@ -199,21 +199,9 @@ export async function render(injector: Injector, struct: ElementStruct | string if ('function' === typeof struct.render) { const frame = stopwatch?.start(struct.render.name, FrameCategory.template); - const element = struct.render as Function & TemplateCacheCall; + const element = struct.render as ((...args: any) => any) & TemplateCacheCall; if (!element.templateCall) { - const type = reflect(struct.render); - if (type.kind === ReflectionKind.function) { - const args: Resolver[] = []; - for (let i = 2; i < type.parameters.length; i++) { - args.push(injector.createResolver(type.parameters[i])); - } - - element.templateCall = (attributes: any, children: any) => { - return element(attributes, children, ...(args.map(v => v()))); - }; - } else { - element.templateCall = element as any; - } + element.templateCall = injectedFunction(element, injector, 2); } try { diff --git a/packages/type-compiler/index.ts b/packages/type-compiler/index.ts index 308a16a83..dc8fbfcc8 100644 --- a/packages/type-compiler/index.ts +++ b/packages/type-compiler/index.ts @@ -9,3 +9,4 @@ */ export * from './src/compiler'; +export * from './src/loader'; diff --git a/packages/type-compiler/install-transformer.ts b/packages/type-compiler/install-transformer.ts index fca4f8b1b..8db1b9bac 100644 --- a/packages/type-compiler/install-transformer.ts +++ b/packages/type-compiler/install-transformer.ts @@ -34,7 +34,6 @@ function getCode(deepkitDistPath: string, varName: string, id: string): string { if (!${varName}.afterDeclarations.includes(typeTransformer.declarationTransformer)) ${varName}.afterDeclarations.push(typeTransformer.declarationTransformer); } } catch (e) { - console.error('failed loading @deepkit/type transformer: ' + e); } `; } diff --git a/packages/type-compiler/package.json b/packages/type-compiler/package.json index cba536543..b3caa270d 100644 --- a/packages/type-compiler/package.json +++ b/packages/type-compiler/package.json @@ -30,15 +30,14 @@ "typescript": "~" }, "devDependencies": { - "@types/lz-string": "^1.3.34", - "@typescript/vfs": "^1.3.5", - "lz-string": "^1.4.4", - "typescript": "^4.6.2" }, "dependencies": { + "@types/lz-string": "^1.3.34", + "@typescript/vfs": "^1.3.5", "@deepkit/type-spec": "^1.0.1-alpha.71", "strip-json-comments": "^3.1.1", - "ts-clone-node": "^0.3.29" + "ts-clone-node": "^0.3.29", + "lz-string": "^1.4.4" }, "jest": { "testEnvironment": "node", @@ -48,6 +47,9 @@ "testMatch": [ "**/tests/**/*.spec.ts" ], + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "globals": { "ts-jest": { "tsconfig": "/tsconfig.test.json" diff --git a/packages/type-compiler/src/compiler.ts b/packages/type-compiler/src/compiler.ts index 8c41c627f..097967c4f 100644 --- a/packages/type-compiler/src/compiler.ts +++ b/packages/type-compiler/src/compiler.ts @@ -8,6 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ +import * as ts from 'typescript'; import { __String, ArrayTypeNode, @@ -16,18 +17,20 @@ import { ClassDeclaration, ClassElement, ClassExpression, + CompilerHost, ConditionalTypeNode, ConstructorDeclaration, ConstructorTypeNode, ConstructSignatureDeclaration, createCompilerHost, createPrinter, - createProgram, + CustomTransformer, CustomTransformerFactory, Declaration, EmitHint, EntityName, EnumDeclaration, + escapeLeadingUnderscores, ExportDeclaration, Expression, ExpressionWithTypeArguments, @@ -57,7 +60,6 @@ import { isFunctionDeclaration, isFunctionExpression, isFunctionLike, - isFunctionTypeNode, isIdentifier, isImportClause, isImportDeclaration, @@ -71,6 +73,8 @@ import { isNamedTupleMember, isObjectLiteralExpression, isOptionalTypeNode, + isParameter, + isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isQualifiedName, @@ -91,7 +95,6 @@ import { Node, NodeFactory, NodeFlags, - Program, PropertyAccessExpression, PropertyDeclaration, PropertySignature, @@ -113,7 +116,7 @@ import { TypeReferenceNode, UnionTypeNode, visitEachChild, - visitNode, + visitNode } from 'typescript'; import { ensureImportIsEmitted, @@ -127,12 +130,14 @@ import { NodeConverter, PackExpression, serializeEntityNameAsExpression, -} from './reflection-ast'; -import { EmitHost, EmitResolver, SourceFile } from './ts-types'; +} from './reflection-ast.js'; +import { SourceFile } from './ts-types.js'; import { existsSync, readFileSync } from 'fs'; import { dirname, join, resolve } from 'path'; import stripJsonComments from 'strip-json-comments'; import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spec'; +import { Resolver } from './resolver.js'; +import { knownLibFilesForCompilerOptions } from '@typescript/vfs'; export function encodeOps(ops: ReflectionOp[]): string { return ops.map(v => String.fromCharCode(v + 33)).join(''); @@ -189,9 +194,6 @@ export function debugPackStruct(sourceFile: SourceFile, forType: Node, pack: { o const op = pack.ops[i]; const opInfo = OPs[op]; items.push(ReflectionOp[op]); - if (ReflectionOp[op] === undefined) { - throw new Error(`Operator ${op} does not exist at position ${i}`); - } if (opInfo && opInfo.params > 0) { for (let j = 0; j < opInfo.params; j++) { const address = pack.ops[++i]; @@ -421,6 +423,31 @@ class CompilerProgram { } } +function getAssignTypeExpression(call: Expression): Expression | undefined { + if (isParenthesizedExpression(call) && isCallExpression(call.expression)) { + call = call.expression; + } + + if (isCallExpression(call) && isIdentifier(call.expression) && getIdentifierName(call.expression) === '__assignType' && call.arguments.length > 0) { + return call.arguments[0]; + } + + return; +} + +function getReceiveTypeParameter(type: TypeNode): TypeReferenceNode | undefined { + if (isUnionTypeNode(type)) { + for (const t of type.types) { + const rfn = getReceiveTypeParameter(t); + if (rfn) return rfn; + } + } else if (isTypeReferenceNode(type) && isIdentifier(type.typeName) + && getIdentifierName(type.typeName) === 'ReceiveType' && !!type.typeArguments + && type.typeArguments.length === 1) return type; + + return; +} + /** * Read the TypeScript AST and generate pack struct (instructions + pre-defined stack). * @@ -428,20 +455,19 @@ class CompilerProgram { * * Deepkit/type can then extract and decode them on-demand. */ -export class ReflectionTransformer { +export class ReflectionTransformer implements CustomTransformer { sourceFile!: SourceFile; - protected program?: Program; - protected host: EmitHost; - protected resolver: EmitResolver; protected f: NodeFactory; + protected embedAssignType: boolean = false; + protected reflectionMode?: typeof reflectionModes[number]; /** * Types added to this map will get a type program directly under it. * This is for types used in the very same file. */ - protected compileDeclarations = new Map(); + protected compileDeclarations = new Map(); /** * Types added to this map will get a type program at the top root level of the program. @@ -458,49 +484,29 @@ export class ReflectionTransformer { protected nodeConverter: NodeConverter; protected typeChecker?: TypeChecker; + protected resolver: Resolver; + protected host: CompilerHost; + + /** + * When an deep call expression was found a script-wide variable is necessary + * as temporary storage. + */ + protected tempResultIdentifier?: Identifier; constructor( protected context: TransformationContext, ) { this.f = context.factory; - this.host = (context as any).getEmitHost(); - this.resolver = (context as any).getEmitResolver(); this.nodeConverter = new NodeConverter(this.f); + this.host = createCompilerHost(context.getCompilerOptions()); + this.resolver = new Resolver(context.getCompilerOptions(), this.host); } - forProgram(program?: Program): this { - this.program = program; + forHost(host: CompilerHost): this { + this.resolver.host = host; return this; } - protected getTypeChecker(file: SourceFile): TypeChecker { - if (this.program) return this.program.getTypeChecker(); - if ((file as any)._typeChecker) return (file as any)._typeChecker; - const options = this.context.getCompilerOptions(); - const host = createCompilerHost(options); - const program = createProgram([file.fileName], this.context.getCompilerOptions(), { ...this.host, ...host }); - // const program = createProgram((this.host as any).getSourceFiles().map((v: SourceFile) => v.fileName), options, { ...this.host, ...host }); - return (file as any)._typeChecker = program.getTypeChecker(); - } - - protected getTypeCheckerForHost(): TypeChecker { - if (this.program) return this.program.getTypeChecker(); - if ((this.host as any)._typeChecker) return (this.host as any)._typeChecker; - const options = this.context.getCompilerOptions(); - const host = createCompilerHost(options); - // const program = createProgram([this.sourceFile.fileName], this.context.getCompilerOptions(), { ...this.host, ...host }); - const program = createProgram((this.host as any).getSourceFiles().map((v: SourceFile) => v.fileName), options, { ...this.host, ...host }); - return (this.host as any)._typeChecker = program.getTypeChecker(); - } - - // protected getTypeChecker(): TypeChecker { - // const sourceFile: SourceFile = this.sourceFile; - // if ((sourceFile as any)._typeChecker) return (sourceFile as any)._typeChecker; - // const host = createCompilerHost(this.context.getCompilerOptions()); - // const program = createProgram([sourceFile.fileName], this.context.getCompilerOptions(), { ...this.host, ...host }); - // return (sourceFile as any)._typeChecker = program.getTypeChecker(); - // } - withReflectionMode(mode: typeof reflectionModes[number]): this { this.reflectionMode = mode; return this; @@ -510,9 +516,33 @@ export class ReflectionTransformer { return node; } + getTempResultIdentifier(): Identifier { + if (this.tempResultIdentifier) return this.tempResultIdentifier; + + const locals = isNodeWithLocals(this.sourceFile) ? this.sourceFile.locals : undefined; + + if (locals) { + let found = 'Ωr'; + for (let i = 0; ; i++) { + found = 'Ωr' + (i ? i : ''); + if (!locals.has(escapeLeadingUnderscores(found))) break; + } + this.tempResultIdentifier = this.f.createIdentifier(found); + } else { + this.tempResultIdentifier = this.f.createIdentifier('Ωr'); + } + return this.tempResultIdentifier; + } + transformSourceFile(sourceFile: SourceFile): SourceFile { if ((sourceFile as any).deepkitTransformed) return sourceFile; (sourceFile as any).deepkitTransformed = true; + this.embedAssignType = false; + + if (!(sourceFile as any).locals) { + //@ts-ignore + ts.bindSourceFile(sourceFile, this.context.getCompilerOptions()); + } this.addImports = []; this.sourceFile = sourceFile; @@ -541,14 +571,14 @@ export class ReflectionTransformer { } } - if (isMethodDeclaration(node) && node.parent && isObjectLiteralExpression(node.parent)) { + if (isMethodDeclaration(node) && node.parent && node.body && isObjectLiteralExpression(node.parent)) { //replace MethodDeclaration with MethodExpression // {add(v: number) {}} => {add: function (v: number) {}} //so that __type can be added const method = this.decorateFunctionExpression( this.f.createFunctionExpression( node.modifiers, node.asteriskToken, isIdentifier(node.name) ? node.name : undefined, - node.typeParameters, node.parameters, node.type, node.body! + node.typeParameters, node.parameters, node.type, node.body ) ); node = this.f.createPropertyAssignment(node.name, method); @@ -556,15 +586,46 @@ export class ReflectionTransformer { if (isClassDeclaration(node)) { return this.decorateClass(node); + } else if (isParameter(node) && node.parent && node.parent.typeParameters && node.type) { + // ReceiveType + const receiveType = getReceiveTypeParameter(node.type); + if (receiveType && receiveType.typeArguments) { + const first = receiveType.typeArguments[0]; + if (first && isTypeReferenceNode(first) && isIdentifier(first.typeName)) { + const name = getIdentifierName(first.typeName); + //find type parameter position + const index = node.parent.typeParameters.findIndex(v => getIdentifierName(v.name) === name); + + let container: Expression = this.f.createIdentifier('globalThis'); + if ((isFunctionDeclaration(node.parent) || isFunctionExpression(node.parent)) && node.parent.name) { + container = node.parent.name; + } else if (isMethodDeclaration(node.parent) && isIdentifier(node.parent.name)) { + container = this.f.createPropertyAccessExpression(this.f.createIdentifier('this'), node.parent.name); + } + + return this.f.updateParameterDeclaration(node, node.decorators, node.modifiers, node.dotDotDotToken, node.name, + node.questionToken, receiveType, this.f.createElementAccessChain( + this.f.createPropertyAccessExpression( + container, + this.f.createIdentifier('Ω'), + ), + this.f.createToken(SyntaxKind.QuestionDotToken), + this.f.createNumericLiteral(index) + ) + ); + } + } } else if (isClassExpression(node)) { return this.decorateClass(node); } else if (isFunctionExpression(node)) { - return this.decorateFunctionExpression(node); + return this.decorateFunctionExpression(this.injectResetΩ(node)); } else if (isFunctionDeclaration(node)) { - return this.decorateFunctionDeclaration(node); + return this.decorateFunctionDeclaration(this.injectResetΩ(node)); + } else if (isMethodDeclaration(node)) { + return this.injectResetΩ(node); } else if (isArrowFunction(node)) { return this.decorateArrow(node); - } else if (isCallExpression(node) && node.typeArguments) { + } else if (isCallExpression(node) && node.typeArguments && node.typeArguments.length > 0) { const autoTypeFunctions = ['valuesOf', 'propertiesOf', 'typeOf']; if (isIdentifier(node.expression) && autoTypeFunctions.includes(getIdentifierName(node.expression))) { const args: Expression[] = [...node.arguments]; @@ -581,94 +642,146 @@ export class ReflectionTransformer { return this.f.updateCallExpression(node, node.expression, node.typeArguments, this.f.createNodeArray(args)); } - let type: Declaration | undefined = undefined; + //put the type argument in FN.Ω + + const expressionToCheck = getAssignTypeExpression(node.expression) || node.expression; + if (isArrowFunction(expressionToCheck)) { + //inline arrow functions are excluded from type passing + return node; + } + + const typeExpressions: Expression[] = []; + for (const a of node.typeArguments) { + const type = this.getTypeOfType(a); + typeExpressions.push(type || this.f.createIdentifier('undefined')); + } + + let container: Expression = this.f.createIdentifier('globalThis'); if (isIdentifier(node.expression)) { - const found = this.resolveDeclaration(node.expression); - if (found) type = found.declaration; + container = node.expression; } else if (isPropertyAccessExpression(node.expression)) { - try { - const found = this.getTypeCheckerForHost().getTypeAtLocation(node.expression); - if (found && found.symbol && found.symbol.declarations) type = found.symbol.declarations[0]; - } catch { - // mysteriously it can fail with "TypeError: Cannot read properties of undefined (reading 'flags')" - // at getTypeFromFlowType (../type-compiler/node_modules/typescript/lib/typescript.js:68607:29) - } + container = node.expression; } - if (type && (isFunctionDeclaration(type) || isMethodDeclaration(type) || isMethodSignature(type) || isFunctionTypeNode(type)) && type.typeParameters) { - const args: Expression[] = [...node.arguments]; - let replaced = false; - - for (let i = 0; i < type.parameters.length; i++) { - const parameter = type.parameters[i]; - const arg = args[i]; - - //we replace from T to this arg only if either not set or set to undefined - if (arg && (!isIdentifier(arg) || arg.escapedText !== 'undefined')) continue; - if (!parameter.type) continue; - - let hasReceiveType: TypeReferenceNode | undefined = undefined; - if (isTypeReferenceNode(parameter.type) && isIdentifier(parameter.type.typeName) && getIdentifierName(parameter.type.typeName) === 'ReceiveType' && parameter.type.typeArguments) { - //check if `fn(p: ReceiveType)` - hasReceiveType = parameter.type; - } else if (isUnionTypeNode(parameter.type)) { - //check if `fn(p: Other | ReceiveType)` - for (const member of parameter.type.types) { - if (isTypeReferenceNode(member) && isIdentifier(member.typeName) && getIdentifierName(member.typeName) === 'ReceiveType' && member.typeArguments) { - hasReceiveType = member; - break; - } - } - } - - if (hasReceiveType && hasReceiveType.typeArguments) { - const first = hasReceiveType.typeArguments[0]; - if (first && isTypeReferenceNode(first) && isIdentifier(first.typeName)) { - const name = getIdentifierName(first.typeName); - //find type parameter position - const index = type.typeParameters.findIndex(v => getIdentifierName(v.name) === name); - if (index !== -1) { - if (!node.typeArguments[index]) continue; - const type = this.getTypeOfType(node.typeArguments[index]); - if (!type) continue; - args[i] = type; - replaced = true; - } - } - } - } + const assignQ = this.f.createBinaryExpression( + this.f.createPropertyAccessExpression(container, 'Ω'), + this.f.createToken(SyntaxKind.EqualsToken), + this.f.createArrayLiteralExpression(typeExpressions), + ); - if (replaced) { - //make sure args has no hole - for (let i = 0; i < args.length; i++) { - if (!args[i]) args[i] = this.f.createIdentifier('undefined'); - } - return this.f.updateCallExpression(node, node.expression, node.typeArguments, this.f.createNodeArray(args)); + if (isPropertyAccessExpression(node.expression)) { + //e.g. http.deep.response(); + if (isCallExpression(node.expression.expression)) { + //e.g. http.deep().response(); + //change to (Ωr = http.deep(), Ωr.response.Ω = [], Ωr).response() + const r = this.getTempResultIdentifier(); + const assignQ = this.f.createBinaryExpression( + this.f.createPropertyAccessExpression( + this.f.createPropertyAccessExpression(r, node.expression.name), + 'Ω' + ), + this.f.createToken(SyntaxKind.EqualsToken), + this.f.createArrayLiteralExpression(typeExpressions), + ); + + return this.f.updateCallExpression(node, + this.f.createPropertyAccessExpression( + this.f.createParenthesizedExpression(this.f.createBinaryExpression( + this.f.createBinaryExpression( + this.f.createBinaryExpression( + r, + this.f.createToken(ts.SyntaxKind.EqualsToken), + node.expression.expression + ), + this.f.createToken(ts.SyntaxKind.CommaToken), + assignQ + ), + this.f.createToken(ts.SyntaxKind.CommaToken), + r + )), + node.expression.name + ), + node.typeArguments, + node.arguments + ) + + } else if (isParenthesizedExpression(node.expression.expression)) { + //e.g. (http.deep()).response(); + //only work necessary when `http.deep()` is using type args and was converted to: + // (Ω = [], http.deep()).response() + + //it's a call like (obj.method.Ω = ['a'], obj.method()).method() + //which needs to be converted so that Ω is correctly read by the last call + //(r = (obj.method.Ω = [['a']], obj.method()), obj.method.Ω = [['b']], r).method()); + + const r = this.getTempResultIdentifier(); + const assignQ = this.f.createBinaryExpression( + this.f.createPropertyAccessExpression( + this.f.createPropertyAccessExpression(r, node.expression.name), + 'Ω' + ), + this.f.createToken(SyntaxKind.EqualsToken), + this.f.createArrayLiteralExpression(typeExpressions), + ); + + const updatedNode = this.f.updateCallExpression( + node, + this.f.updatePropertyAccessExpression( + node.expression, + this.f.updateParenthesizedExpression( + node.expression.expression, + this.f.createBinaryExpression( + this.f.createBinaryExpression( + this.f.createBinaryExpression( + r, + this.f.createToken(SyntaxKind.EqualsToken), + node.expression.expression.expression, + ), + this.f.createToken(SyntaxKind.CommaToken), + assignQ + ), + this.f.createToken(SyntaxKind.CommaToken), + r, + ) + ), + node.expression.name + ), + node.typeArguments, + node.arguments + ); + + return this.f.createParenthesizedExpression(updatedNode); + } else { + //e.g. http.deep.response(); + //nothing to do } } + + //(fn.Ω = [], call()) + return this.f.createParenthesizedExpression(this.f.createBinaryExpression( + assignQ, + this.f.createToken(SyntaxKind.CommaToken), + node, + )); } return node; }; this.sourceFile = visitNode(this.sourceFile, visitor); - //externalize type aliases - const compileDeclarations = (node: Node): any => { - node = visitEachChild(node, compileDeclarations, this.context); - - if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isEnumDeclaration(node)) && this.compileDeclarations.has(node)) { - const d = this.compileDeclarations.get(node)!; - this.compileDeclarations.delete(node); - this.compiledDeclarations.add(node); - return [...this.createProgramVarFromNode(node, d.name, d.sourceFile), node]; + while (true) { + let allCompiled = true; + for (const d of this.compileDeclarations.values()) { + if (d.compiled) continue; + allCompiled = false; + break; } - return node; - }; + if (this.embedDeclarations.size === 0 && allCompiled) break; - while (this.compileDeclarations.size || this.embedDeclarations.size) { - if (this.compileDeclarations.size) { - this.sourceFile = visitNode(this.sourceFile, compileDeclarations); + for (const [node, d] of [...this.compileDeclarations.entries()]) { + if (d.compiled) continue; + d.compiled = this.createProgramVarFromNode(node, d.name, this.sourceFile); } if (this.embedDeclarations.size) { @@ -685,9 +798,29 @@ export class ReflectionTransformer { } } + //externalize type aliases + const compileDeclarations = (node: Node): any => { + node = visitEachChild(node, compileDeclarations, this.context); + + if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node) || isEnumDeclaration(node))) { + const d = this.compileDeclarations.get(node); + if (!d) { + return node; + } + this.compileDeclarations.delete(node); + this.compiledDeclarations.add(node); + if (d.compiled) { + return [...d.compiled, node]; + } + } + + return node; + }; + this.sourceFile = visitNode(this.sourceFile, compileDeclarations); + + const embedTopExpression: Statement[] = []; if (this.addImports.length) { const compilerOptions = this.context.getCompilerOptions(); - const imports: Statement[] = []; const handledIdentifier: string[] = []; for (const imp of this.addImports) { if (handledIdentifier.includes(getIdentifierName(imp.identifier))) continue; @@ -699,7 +832,7 @@ export class ReflectionTransformer { undefined, undefined, this.f.createCallExpression(this.f.createIdentifier('require'), undefined, [imp.from]) )], NodeFlags.Const)); - imports.push(variable); + embedTopExpression.push(variable); } else { //import {identifier} from './bar' // import { identifier as identifier } is used to avoid automatic elision of imports (in angular builds for example) @@ -709,11 +842,76 @@ export class ReflectionTransformer { const importStatement = this.f.createImportDeclaration(undefined, undefined, this.f.createImportClause(false, undefined, namedImports), imp.from ); - imports.push(importStatement); + embedTopExpression.push(importStatement); } } + } + + if (this.embedAssignType) { + const assignType = this.f.createFunctionDeclaration( + undefined, + undefined, + undefined, + this.f.createIdentifier('__assignType'), + undefined, + [ + this.f.createParameterDeclaration( + undefined, + undefined, + undefined, + this.f.createIdentifier('fn'), + undefined, + undefined, //this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword), + undefined + ), + this.f.createParameterDeclaration( + undefined, + undefined, + undefined, + this.f.createIdentifier('args'), + undefined, + undefined, //this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword), + undefined + ) + ], + undefined, //this.f.createKeywordTypeNode(SyntaxKind.AnyKeyword), + this.f.createBlock( + [ + this.f.createExpressionStatement(this.f.createBinaryExpression( + this.f.createPropertyAccessExpression( + this.f.createIdentifier('fn'), + this.f.createIdentifier('__type') + ), + this.f.createToken(SyntaxKind.EqualsToken), + this.f.createIdentifier('args') + )), + this.f.createReturnStatement(this.f.createIdentifier('fn')) + ], + true + ) + ); + embedTopExpression.push(assignType); + } - this.sourceFile = this.f.updateSourceFile(this.sourceFile, [...imports, ...this.sourceFile.statements]); + if (this.tempResultIdentifier) { + embedTopExpression.push( + this.f.createVariableStatement( + undefined, + this.f.createVariableDeclarationList( + [this.f.createVariableDeclaration( + this.tempResultIdentifier, + undefined, + undefined, + undefined + )], + ts.NodeFlags.None + ) + ) + ); + } + + if (embedTopExpression.length) { + this.sourceFile = this.f.updateSourceFile(this.sourceFile, [...embedTopExpression, ...this.sourceFile.statements]); } // console.log('transform sourceFile', this.sourceFile.fileName); @@ -721,7 +919,45 @@ export class ReflectionTransformer { return this.sourceFile; } - protected createProgramVarFromNode(node: Node, name: EntityName, sourceFile: SourceFile) { + protected injectResetΩ(node: T): T { + let hasReceiveType = false; + if (!node.typeParameters) return node; + for (const param of node.parameters) { + if (param.type && getReceiveTypeParameter(param.type)) hasReceiveType = true; + } + if (!hasReceiveType) return node; + + let container: Expression = this.f.createIdentifier('globalThis'); + if ((isFunctionDeclaration(node) || isFunctionExpression(node)) && node.name) { + container = node.name; + } else if (isMethodDeclaration(node) && isIdentifier(node.name)) { + container = this.f.createPropertyAccessExpression(this.f.createIdentifier('this'), node.name); + } + + const reset: Statement = this.f.createExpressionStatement(this.f.createBinaryExpression( + this.f.createPropertyAccessExpression( + container, + this.f.createIdentifier('Ω') + ), + this.f.createToken(ts.SyntaxKind.EqualsToken), + this.f.createIdentifier('undefined') + )); + const body = node.body ? this.f.updateBlock(node.body, [reset, ...node.body.statements]) : undefined; + + if (isFunctionDeclaration(node)) { + return this.f.updateFunctionDeclaration(node, node.decorators, node.modifiers, node.asteriskToken, node.name, + node.typeParameters, node.parameters, node.type, body) as T; + } else if (isFunctionExpression(node)) { + return this.f.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, + node.typeParameters, node.parameters, node.type, body || node.body) as T; + } else if (isMethodDeclaration(node)) { + return this.f.updateMethodDeclaration(node, node.decorators, node.modifiers, node.asteriskToken, node.name, + node.questionToken, node.typeParameters, node.parameters, node.type, body) as T; + } + return node; + } + + protected createProgramVarFromNode(node: Node, name: EntityName, sourceFile: SourceFile): Statement[] { const typeProgram = new CompilerProgram(node, sourceFile); if ((isTypeAliasDeclaration(node) || isInterfaceDeclaration(node)) && node.typeParameters) { @@ -765,7 +1001,6 @@ export class ReflectionTransformer { return [variable, exportNode]; } - return [variable]; } @@ -1087,7 +1322,7 @@ export class ReflectionTransformer { const falseProgram = program.popCoRoutine(); program.pushOp(ReflectionOp.jumpCondition, trueProgram, falseProgram); - program.moveFrame(); + program.moveFrame(); //pops frame if (distributiveOverIdentifier) { const coRoutineIndex = program.popCoRoutine(); @@ -1157,7 +1392,7 @@ export class ReflectionTransformer { if (hasModifier(parameter, SyntaxKind.PrivateKeyword)) program.pushOp(ReflectionOp.private); if (hasModifier(parameter, SyntaxKind.ProtectedKeyword)) program.pushOp(ReflectionOp.protected); if (hasModifier(narrowed, SyntaxKind.ReadonlyKeyword)) program.pushOp(ReflectionOp.readonly); - if (parameter.initializer) { + if (parameter.initializer && parameter.type && !getReceiveTypeParameter(parameter.type)) { program.pushOp(ReflectionOp.defaultValue, program.findOrAddStackEntry(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, parameter.initializer))); } } @@ -1347,6 +1582,25 @@ export class ReflectionTransformer { 'Boolean': ReflectionOp.boolean, }; + protected globalSourceFiles?: SourceFile[]; + + protected getGlobalLibs(): SourceFile[] { + if (this.globalSourceFiles) return this.globalSourceFiles; + + this.globalSourceFiles = []; + + //todo also read compiler options "types" + typeRoot + + const libs = knownLibFilesForCompilerOptions(this.context.getCompilerOptions(), ts); + + for (const lib of libs) { + const sourceFile = this.resolver.resolveSourceFile(this.sourceFile.fileName, 'typescript/lib/' + lib.replace('.d.ts', '')); + if (!sourceFile) continue; + this.globalSourceFiles.push(sourceFile); + } + return this.globalSourceFiles; + } + /** * This is a custom resolver based on populated `locals` from the binder. It uses a custom resolution algorithm since * we have no access to the binder/TypeChecker directly and instantiating a TypeChecker per file/transformer is incredible slow. @@ -1371,8 +1625,8 @@ export class ReflectionTransformer { } if (!declaration) { - //look in globals, read through all files, see checker.ts initializeTypeChecker - for (const file of this.host.getSourceFiles()) { + // look in globals, read through all files, see checker.ts initializeTypeChecker + for (const file of this.getGlobalLibs()) { const globals = getGlobalsOfSourceFile(file); if (!globals) continue; const symbol = globals.get(typeName.escapedText); @@ -1382,7 +1636,6 @@ export class ReflectionTransformer { break; } } - // console.log('look in global'); } let importDeclaration: ImportDeclaration | undefined = undefined; @@ -1400,7 +1653,7 @@ export class ReflectionTransformer { if (importDeclaration) { if (importDeclaration.importClause && importDeclaration.importClause.isTypeOnly) typeOnly = true; - declaration = this.resolveImportSpecifier(typeName.escapedText, importDeclaration); + declaration = this.resolveImportSpecifier(typeName.escapedText, importDeclaration, this.sourceFile); } if (declaration && declaration.kind === SyntaxKind.TypeParameter && declaration.parent.kind === SyntaxKind.TypeAliasDeclaration) { @@ -1523,7 +1776,7 @@ export class ReflectionTransformer { } } - //non existing references are ignored. + //non-existing references are ignored. program.pushOp(ReflectionOp.never); return; } @@ -1575,7 +1828,7 @@ export class ReflectionTransformer { } //to break recursion, we track which declaration has already been compiled - if (!this.compiledDeclarations.has(declaration)) { + if (!this.compiledDeclarations.has(declaration) && !this.compileDeclarations.has(declaration)) { const declarationSourceFile = findSourceFile(declaration) || this.sourceFile; const isGlobal = resolved.importDeclaration === undefined && declarationSourceFile.fileName !== this.sourceFile.fileName; const isFromImport = resolved.importDeclaration !== undefined; @@ -1594,20 +1847,30 @@ export class ReflectionTransformer { return; } - //check if the referenced declaration has reflection disabled + // //check if the referenced declaration has reflection disabled const declarationReflection = this.findReflectionConfig(declaration, program); if (declarationReflection.mode === 'never') { program.pushOp(ReflectionOp.any); return; } - const found = this.resolver.getExternalModuleFileFromDeclaration(resolved.importDeclaration); + const found = this.resolver.resolve(this.sourceFile, resolved.importDeclaration); if (!found) { debug('module not found'); program.pushOp(ReflectionOp.any); return; } + // check if this is a viable option: + // //check if the referenced file has reflection info emitted. if not, any is emitted for that reference + // const typeVar = this.getDeclarationVariableName(typeName); + // //check if typeVar is exported in referenced file + // const builtType = isNodeWithLocals(found) && found.locals && found.locals.has(typeVar.escapedText); + // if (!builtType) { + // program.pushOp(ReflectionOp.any); + // return; + // } + //check if the referenced file has reflection info emitted. if not, any is emitted for that reference const reflection = this.findReflectionFromPath(found.fileName); if (reflection.mode === 'never') { @@ -1615,9 +1878,11 @@ export class ReflectionTransformer { return; } + // this.addImports.push({ identifier: typeVar, from: resolved.importDeclaration.moduleSpecifier }); this.addImports.push({ identifier: this.getDeclarationVariableName(typeName), from: resolved.importDeclaration.moduleSpecifier }); } } else { + //it's a reference type inside the same file. Make sure its type is reflected const reflection = this.findReflectionConfig(declaration, program); if (reflection.mode === 'never') { program.pushOp(ReflectionOp.any); @@ -1860,17 +2125,11 @@ export class ReflectionTransformer { return; } - protected resolveImportSpecifier(declarationName: __String, importOrExport: ExportDeclaration | ImportDeclaration): Declaration | undefined { + protected resolveImportSpecifier(declarationName: __String, importOrExport: ExportDeclaration | ImportDeclaration, sourceFile: SourceFile): Declaration | undefined { if (!importOrExport.moduleSpecifier) return; if (!isStringLiteral(importOrExport.moduleSpecifier)) return; - let source: SourceFile | ModuleDeclaration | undefined = this.resolver.getExternalModuleFileFromDeclaration(importOrExport); - if (!source) { - const found = this.getTypeCheckerForHost().getSymbolAtLocation(importOrExport.moduleSpecifier); - if (found && found.valueDeclaration && isModuleDeclaration(found.valueDeclaration)) { - source = found.valueDeclaration; - } - } + let source: SourceFile | ModuleDeclaration | undefined = this.resolver.resolve(sourceFile, importOrExport); if (!source) { debug('module not found', (importOrExport as any).text, 'Is transpileOnly enabled? It needs to be disabled.'); @@ -1885,7 +2144,7 @@ export class ReflectionTransformer { if (declaration && !isImportSpecifier(declaration)) { //if `export {PrimaryKey} from 'xy'`, then follow xy if (isExportDeclaration(declaration)) { - return this.followExport(declarationName, declaration); + return this.followExport(declarationName, declaration, source); } return declaration; } @@ -1894,7 +2153,7 @@ export class ReflectionTransformer { if (isSourceFile(source)) { for (const statement of source.statements) { if (!isExportDeclaration(statement)) continue; - const found = this.followExport(declarationName, statement); + const found = this.followExport(declarationName, statement, source); if (found) return found; } } @@ -1902,14 +2161,14 @@ export class ReflectionTransformer { return; } - protected followExport(declarationName: __String, statement: ExportDeclaration): Declaration | undefined { + protected followExport(declarationName: __String, statement: ExportDeclaration, sourceFile: SourceFile): Declaration | undefined { if (statement.exportClause) { //export {y} from 'x' if (isNamedExports(statement.exportClause)) { for (const element of statement.exportClause.elements) { //see if declarationName is exported if (element.name.escapedText === declarationName) { - const found = this.resolveImportSpecifier(element.propertyName ? element.propertyName.escapedText : declarationName, statement); + const found = this.resolveImportSpecifier(element.propertyName ? element.propertyName.escapedText : declarationName, statement, sourceFile); if (found) return found; } } @@ -1917,7 +2176,7 @@ export class ReflectionTransformer { } else { //export * from 'x' //see if `x` exports declarationName (or one of its exports * from 'y') - const found = this.resolveImportSpecifier(declarationName, statement); + const found = this.resolveImportSpecifier(declarationName, statement, sourceFile); if (found) { return found; } @@ -1989,13 +2248,7 @@ export class ReflectionTransformer { const encodedType = this.getTypeOfType(expression); if (!encodedType) return expression; - const __type = this.f.createObjectLiteralExpression([ - this.f.createPropertyAssignment('__type', encodedType) - ]); - - return this.f.createCallExpression(this.f.createPropertyAccessExpression(this.f.createIdentifier('Object'), 'assign'), undefined, [ - expression, __type - ]); + return this.wrapWithAssignType(expression, encodedType); } /** @@ -2023,13 +2276,27 @@ export class ReflectionTransformer { const encodedType = this.getTypeOfType(expression); if (!encodedType) return expression; - const __type = this.f.createObjectLiteralExpression([ - this.f.createPropertyAssignment('__type', encodedType) - ]); + return this.wrapWithAssignType(expression, encodedType); + } - return this.f.createCallExpression(this.f.createPropertyAccessExpression(this.f.createIdentifier('Object'), 'assign'), undefined, [ - expression, __type - ]); + /** + * Object.assign(fn, {__type: []}) is much slower than a custom implementation like + * + * assignType(fn, []) + * + * where we embed assignType() at the beginning of the type. + */ + protected wrapWithAssignType(fn: Expression, type: Expression) { + this.embedAssignType = true; + + return this.f.createCallExpression( + this.f.createIdentifier('__assignType'), + undefined, + [ + fn, + type + ] + ); } protected parseReflectionMode(mode?: typeof reflectionModes[number] | '' | boolean | string[], configPathDir?: string): typeof reflectionModes[number] { @@ -2048,6 +2315,7 @@ export class ReflectionTransformer { } protected resolvedTsConfig: { [path: string]: { data: Record, exists: boolean } } = {}; + protected resolvedPackageJson: { [path: string]: { data: Record, exists: boolean } } = {}; protected findReflectionConfig(node: Node, program?: CompilerProgram): { mode: typeof reflectionModes[number] } { if (program && program.sourceFile.fileName !== this.sourceFile.fileName) { @@ -2089,31 +2357,63 @@ export class ReflectionTransformer { let reflection: typeof reflectionModes[number] | undefined; while (currentDir) { - const tsconfigPath = join(currentDir, 'tsconfig.json'); - const packageJson = join(currentDir, 'package.json'); + const packageJsonPath = join(currentDir, 'package.json'); + const tsConfigPath = join(currentDir, 'tsconfig.json'); + + let packageJson: Record = {}; let tsConfig: Record = {}; - const cache = this.resolvedTsConfig[tsconfigPath]; - if (cache) { - tsConfig = this.resolvedTsConfig[tsconfigPath].data; + + const packageJsonCache = this.resolvedPackageJson[packageJsonPath]; + let packageJsonExists = false; + + if (packageJsonCache) { + packageJson = packageJsonCache.data; + packageJsonExists = packageJsonCache.exists; + } else { + packageJsonExists = existsSync(packageJsonPath); + this.resolvedPackageJson[packageJsonPath] = { exists: packageJsonExists, data: {} }; + if (packageJsonExists) { + try { + let content = readFileSync(packageJsonPath, 'utf8'); + content = stripJsonComments(content); + packageJson = JSON.parse(content); + this.resolvedPackageJson[packageJsonPath].data = packageJson; + } catch (error: any) { + console.warn(`Could not parse ${packageJsonPath}: ${error}`); + } + } + } + + const tsConfigCache = this.resolvedTsConfig[tsConfigPath]; + let tsConfigExists = false; + + if (tsConfigCache) { + tsConfig = tsConfigCache.data; + tsConfigExists = tsConfigCache.exists; } else { - const exists = existsSync(tsconfigPath); - this.resolvedTsConfig[tsconfigPath] = { exists, data: {} }; - if (exists) { + tsConfigExists = existsSync(tsConfigPath); + this.resolvedTsConfig[tsConfigPath] = { exists: tsConfigExists, data: {} }; + if (tsConfigExists) { try { - let content = readFileSync(tsconfigPath, 'utf8'); + let content = readFileSync(tsConfigPath, 'utf8'); content = stripJsonComments(content); tsConfig = JSON.parse(content); - this.resolvedTsConfig[tsconfigPath].data = tsConfig; + this.resolvedTsConfig[tsConfigPath].data = tsConfig; } catch (error: any) { - console.warn(`Could not parse ${tsconfigPath}: ${error}`); + console.warn(`Could not parse ${tsConfigPath}: ${error}`); } } } + + if (reflection === undefined && packageJson.reflection !== undefined) { + return { mode: this.parseReflectionMode(packageJson.reflection, currentDir) }; + } + if (reflection === undefined && tsConfig.reflection !== undefined) { return { mode: this.parseReflectionMode(tsConfig.reflection, currentDir) }; } - if (existsSync(packageJson)) { + if (packageJsonExists) { //we end the search at package.json so that package in node_modules without reflection option //do not inherit the tsconfig from the project. break; diff --git a/packages/type-compiler/src/loader.ts b/packages/type-compiler/src/loader.ts new file mode 100644 index 000000000..9b29a7014 --- /dev/null +++ b/packages/type-compiler/src/loader.ts @@ -0,0 +1,59 @@ +// import {urlToRequest} from 'loader-utils'; +import * as ts from 'typescript'; +import { CompilerOptions, createCompilerHost, createSourceFile, ScriptTarget, SourceFile, TransformationContext } from 'typescript'; +import { ReflectionTransformer } from './compiler.js'; +import ScriptKind = ts.ScriptKind; + +export class DeepkitLoader { + protected options: CompilerOptions = { + allowJs: true, + declaration: false, + }; + + protected host = createCompilerHost(this.options); + + protected program = ts.createProgram([], this.options, this.host); + + protected printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + + protected knownFiles: { [path: string]: string } = {}; + protected sourceFiles: { [path: string]: SourceFile } = {}; + + constructor() { + const originReadFile = this.host.readFile; + this.host.readFile = (fileName: string) => { + if (this.knownFiles[fileName]) return this.knownFiles[fileName]; + return originReadFile.call(this.host, fileName); + }; + + //the program should not write any files + this.host.writeFile = () => { + }; + + const originalGetSourceFile = this.host.getSourceFile; + this.host.getSourceFile = (fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined => { + if (this.sourceFiles[fileName]) return this.sourceFiles[fileName]; + return originalGetSourceFile.call(this.host, fileName, languageVersion, onError, shouldCreateNewSourceFile); + }; + } + + transform(source: string, path: string): string { + this.knownFiles[path] = source; + const sourceFile = createSourceFile(path, source, ScriptTarget.ESNext, true, path.endsWith('.tsx') ? ScriptKind.TSX : ScriptKind.TS); + let newSource = source; + + ts.transform(sourceFile, [ + (context: TransformationContext) => { + const transformer = new ReflectionTransformer(context).forHost(this.host).withReflectionMode('always'); + return (node: SourceFile): SourceFile => { + const sourceFile = transformer.transformSourceFile(node); + + newSource = this.printer.printNode(ts.EmitHint.SourceFile, sourceFile, sourceFile); + return sourceFile; + }; + } + ], this.options); + + return newSource; + } +} diff --git a/packages/type-compiler/src/reflection-ast.ts b/packages/type-compiler/src/reflection-ast.ts index 74b0e8fc5..8f185c135 100644 --- a/packages/type-compiler/src/reflection-ast.ts +++ b/packages/type-compiler/src/reflection-ast.ts @@ -180,6 +180,7 @@ export function isNodeWithLocals(node: Node): node is (Node & { locals: SymbolTa return 'locals' in node; } +//logic copied from typescript export function getGlobalsOfSourceFile(file: SourceFile): SymbolTable | void { if (file.redirectInfo) return; if (!isNodeWithLocals(file)) return; diff --git a/packages/type-compiler/src/resolver.ts b/packages/type-compiler/src/resolver.ts new file mode 100644 index 000000000..3559ce714 --- /dev/null +++ b/packages/type-compiler/src/resolver.ts @@ -0,0 +1,58 @@ +import * as ts from 'typescript'; +import { + CompilerHost, + CompilerOptions, + createSourceFile, + ExportDeclaration, + Expression, + ImportDeclaration, + resolveModuleName, + ScriptTarget, + SourceFile, + StringLiteral, + SyntaxKind +} from 'typescript'; + +/** + * A utility to resolve a module path and its declaration. + * + * It automatically reads a SourceFile and binds it. + */ +export class Resolver { + protected sourceFiles: { [fileName: string]: SourceFile } = {}; + + constructor(public compilerOptions: CompilerOptions, public host: CompilerHost) { + } + + resolve(from: SourceFile, importOrExportNode: ExportDeclaration | ImportDeclaration): SourceFile | undefined { + const moduleSpecifier: Expression | undefined = importOrExportNode.moduleSpecifier; + if (!moduleSpecifier) return; + if (moduleSpecifier.kind !== SyntaxKind.StringLiteral) return; + + return this.resolveSourceFile(from.fileName, (moduleSpecifier as StringLiteral).text); + } + + /** + * Tries to resolve the d.ts file path for a given module path. + * Scans relative paths. Looks into package.json "types" and "exports" (with new 4.7 support) + * + * @param fromPath the path of the file that contains the import. modulePath is relative to that. + * @param modulePath the x in 'from x'. + */ + resolveSourceFile(fromPath: string, modulePath: string): SourceFile | undefined { + const result = resolveModuleName(modulePath, fromPath, this.compilerOptions, this.host); + if (!result.resolvedModule) return; + + const fileName = result.resolvedModule.resolvedFileName; + if (this.sourceFiles[fileName]) return this.sourceFiles[fileName]; + + const source = this.host.readFile(result.resolvedModule.resolvedFileName); + if (!source) return; + const sourceFile = this.sourceFiles[fileName] = createSourceFile(fileName, source, this.compilerOptions.target || ScriptTarget.ES2018, true); + + //@ts-ignore + ts.bindSourceFile(sourceFile, this.compilerOptions); + + return sourceFile; + } +} diff --git a/packages/type-compiler/src/ts-types.ts b/packages/type-compiler/src/ts-types.ts index 08c55cd91..3189bac46 100644 --- a/packages/type-compiler/src/ts-types.ts +++ b/packages/type-compiler/src/ts-types.ts @@ -2,19 +2,7 @@ //Certain interfaces do not contain all properties/methods from all internal TS types, because we add only those we actually use. //This helps to identity which types are actually needed and maybe can be brought up to the TS team as candidates to make them public. -import { - CompilerOptions, - ExportDeclaration, - ImportCall, - ImportDeclaration, - ImportEqualsDeclaration, - ImportTypeNode, - ModuleDeclaration, - Path, - SourceFile as TSSourceFile, - Symbol, - SymbolTable -} from 'typescript'; +import { SourceFile as TSSourceFile, Symbol, SymbolTable } from 'typescript'; /** * Contains @internal properties that are not yet in the public API of TS. @@ -36,43 +24,3 @@ export interface SourceFile extends TSSourceFile { //part of Node symbol?: Symbol; // Symbol declared by node (initialized by binding) } - -//redefine because we patched SourceFile -export interface ScriptReferenceHost { - getCompilerOptions(): CompilerOptions; - - getSourceFile(fileName: string): SourceFile | undefined; - - getSourceFileByPath(path: Path): SourceFile | undefined; - - getCurrentDirectory(): string; -} - -//empty because we don't use any of these methods -export interface ModuleSpecifierResolutionHost { -} - -//empty because we don't use any of these methods -export interface SourceFileMayBeEmittedHost { - -} - -//not all methods included since we only use a subset. -export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost, SourceFileMayBeEmittedHost { - getSourceFiles(): readonly SourceFile[]; - - useCaseSensitiveFileNames(): boolean; - - getCurrentDirectory(): string; -} - -/** - * An internal helper that has not yet exposed to transformers. - */ -export interface EmitResolver { - // getReferencedValueDeclaration(reference: Identifier): Declaration | undefined; - - // getReferencedImportDeclaration(nodeIn: Identifier): Declaration | undefined; - - getExternalModuleFileFromDeclaration(declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode | ImportCall): SourceFile | undefined; -} diff --git a/packages/type-compiler/tests/compiler-vfs.spec.ts b/packages/type-compiler/tests/compiler-vfs.spec.ts deleted file mode 100644 index 246a4a148..000000000 --- a/packages/type-compiler/tests/compiler-vfs.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as ts from 'typescript'; -import { expect, test } from '@jest/globals'; -import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost } from "@typescript/vfs" -import { ReflectionTransformer, transformer } from '../src/compiler'; -import { getPreEmitDiagnostics, TransformationContext } from 'typescript'; - -test('asd', () => { - const compilerOptions: ts.CompilerOptions = { - target: ts.ScriptTarget.ES2015, - allowNonTsExtensions: true, - module: ts.ModuleKind.CommonJS, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - experimentalDecorators: true, - esModuleInterop: true, - }; - const fsMap = createDefaultMapFromNodeModules({ target: ts.ScriptTarget.ES2015 }) - // const fsMap = new Map() - fsMap.set('index.ts', ` - import {PrimaryKey} from '@deepkit/type'; - - class User { - id: PrimaryKey & number = 0; - } - `); - fsMap.set('/node_modules/@deepkit/type/index.d.ts', ` - export type PrimaryKey = { - __meta?: ['primaryKey']; - }; - export type __ΩPrimaryKey = any[]; - `); - - const system = createSystem(fsMap); - fsMap.delete('/lib.webworker.d.ts'); //this throws type errors otherwise - - const host = createVirtualCompilerHost(system, compilerOptions, ts); - - // const fileExists = host.compilerHost.fileExists; - // host.compilerHost.fileExists = fileName => { - // // console.log('fileExists', fileName); - // return fileExists(fileName); - // }; - // const readFile = host.compilerHost.readFile; - // host.compilerHost.readFile = fileName => { - // // console.log('readFile', fileName); - // return readFile(fileName); - // }; - // - // const getSourceFile = host.compilerHost.getSourceFile; - // host.compilerHost.getSourceFile = (...args: any[]) => { - // const content = (getSourceFile as any)(...args); - // // console.log('getSourceFile', args, content); - // return content; - // }; - - const program = ts.createProgram({ - rootNames: [...fsMap.keys()], - options: compilerOptions, - host: host.compilerHost, - }); - for (const d of getPreEmitDiagnostics(program)) { - console.log('diagnostics', d.file?.fileName, d.messageText , d.start, d.length); - } - - program.emit(undefined, undefined, undefined, undefined, { - before: [(context: TransformationContext) => new ReflectionTransformer(context).withReflectionMode('always')], - }); - - const result = fsMap.get('index.js'); - - console.log('generated', result); - expect(result).toContain(`var { __ΩPrimaryKey } = require('@deepkit/type');`); - expect(result).toContain(`User.__type = [() => __ΩPrimaryKey, 'id'`); -}) diff --git a/packages/type-compiler/tests/transform.spec.ts b/packages/type-compiler/tests/transform.spec.ts new file mode 100644 index 000000000..6e73916c3 --- /dev/null +++ b/packages/type-compiler/tests/transform.spec.ts @@ -0,0 +1,112 @@ +import * as ts from 'typescript'; +import { createSourceFile, ScriptTarget } from 'typescript'; +import { expect, test } from '@jest/globals'; +import { ReflectionTransformer } from '../src/compiler'; +import { transform } from './utils'; + +test('transform simple', () => { + const sourceFile = createSourceFile('app.ts', ` + import { Logger } from './logger'; + + function fn(logger: Logger) {} + `, ScriptTarget.ESNext); + + const res = ts.transform(sourceFile, [(context) => (node) => new ReflectionTransformer(context).withReflectionMode('always').transformSourceFile(node)]); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const code = printer.printNode(ts.EmitHint.SourceFile, res.transformed[0], res.transformed[0]); + + console.log(code); +}); + +test('transform util', () => { + const res = transform({ app: `function log(message: string) {}` }); + expect(res.app).toContain('log.__type = '); +}); + +test('resolve import ts', () => { + const res = transform({ + 'app': ` + import { Logger } from './logger'; + function fn(logger: Logger) {} + `, + 'logger': `export class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => Logger'); + expect(res.logger).toContain('static __type'); +}); + +test('resolve import d.ts', () => { + const res = transform({ + 'app': ` + import { Logger } from './logger'; + function fn(logger: Logger) {} + `, + 'logger.d.ts': `export declare class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => Logger'); +}); + +test('resolve import node_modules', () => { + const res = transform({ + 'app': ` + import { Logger } from 'logger'; + function fn(logger: Logger) {} + `, + 'node_modules/logger/index.d.ts': `export declare class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => Logger'); +}); + +test('pass type argument named function', () => { + const res = transform({ + 'app': ` + function getType(type?: ReceiveType) { + } + + getType(); + ` + }); + + console.log(res); + expect(res.app).toContain(`(getType.Ω = `); + expect(res.app).toContain(`, getType())`); +}); + +test('pass type argument arrow function', () => { + const res = transform({ + 'app': ` + ((type?: ReceiveType) => {})(); + ` + }); + + console.log(res); +}); + +test('globals', () => { + const res = transform({ + 'app': ` + interface User {} + export type a = Partial; + ` + }); + + //we just make sure the global was detected and embedded + expect(res.app).toContain('const __ΩPartial = '); + expect(res.app).toContain('() => __ΩPartial'); +}); + +test('class expression', () => { + const res = transform({ + 'app': ` + const a = class {}; + ` + }); + + expect(res.app).toContain('static __type = ['); +}); diff --git a/packages/type-compiler/tests/transpile.spec.ts b/packages/type-compiler/tests/transpile.spec.ts new file mode 100644 index 000000000..83f55e7e6 --- /dev/null +++ b/packages/type-compiler/tests/transpile.spec.ts @@ -0,0 +1,255 @@ +import { expect, test } from '@jest/globals'; +import { transpile, transpileAndRun } from './utils'; + +test('function __type', () => { + const res = transpile({ app: `function log(message: string) {}` }); + console.log(res); + expect(res.app).toContain('log.__type = '); +}); + +test('resolve import ts', () => { + const res = transpile({ + 'app': ` + import { Logger } from './logger'; + function fn(logger: Logger) {} + `, + 'logger': `export class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => logger_1.Logger'); + expect(res.logger).toContain('Logger.__type ='); +}); + +test('resolve import d.ts', () => { + const res = transpile({ + 'app': ` + import { Logger } from './logger'; + function fn(logger: Logger) {} + `, + 'logger.d.ts': `export declare class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => logger_1.Logger'); +}); + +test('resolve import node_modules', () => { + const res = transpile({ + 'app': ` + import { Logger } from 'logger'; + function fn(logger: Logger) {} + `, + 'node_modules/logger/index.d.ts': `export declare class Logger {}` + }); + + console.log(res); + expect(res.app).toContain('() => logger_1.Logger'); +}); + +test('pass type argument named function', () => { + const res = transpile({ + 'app': ` + function getType(type?: ReceiveType) { + } + + getType(); + ` + }); + + console.log(res); +}); + +test('pass type argument named function second param', () => { + const res = transpile({ + 'app': ` + type ReceiveType = Packed | Type | ClassType; + + function getType(first: string = 1, type?: ReceiveType) { + } + + getType(); + ` + }); + + console.log(res); +}); + +test('pass type argument property access', () => { + const res = transpile({ + 'app': ` + class Database { + query(type?: ReceiveType) {} + } + + const db = new Database; + db.query(); + ` + }); + + console.log(res); +}); + +test('pass type argument arrow function', () => { + const res = transpile({ + 'app': ` + ((type?: ReceiveType) => {})(); + ` + }); + + console.log(res); +}); + +test('globals', () => { + const res = transpile({ + 'app': ` + interface User {} + export type a = Partial; + ` + }); + + expect(res.app).toContain('const __ΩPartial = '); + expect(res.app).toContain('() => __ΩPartial'); +}); + +test('chained methods two calls', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + class Http { + response(type?: ReceiveType) { + types.push(type); + return this; + } + } + const http = new Http; + http.response<1>().response<2>(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([[1, '.!'], [2, '.!']]); +}); + +test('chained methods two calls, one without', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + class Http { + response(type?: ReceiveType) { + types.push(type); + return this; + } + } + const http = new Http; + http.response().response<2>(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([undefined, [2, '.!']]); +}); + +test('chained methods three calls', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + class Http { + response(type?: ReceiveType) { + types.push(type); + return this; + } + } + const http = new Http; + http.response<1>().response<2>().response<3>(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([[1, '.!'], [2, '.!'], [3, '.!']]); +}); + +test('chained methods three calls one without', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + class Http { + GET(path: string) { return this } + response(n: number, desc: string, type?: ReceiveType) { + types.push(type); + return this; + } + } + const http = new Http; + http.GET('/action3') + .response<2>(200, \`List\`) + .response<3>(400, \`Error\`); + + types; + ` + }); + + console.log(res); + expect(res).toEqual([[2, '.!'], [3, '.!']]); +}); + +test('multiple calls optional types', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + function add(type?: ReceiveType) { + types.push(type); + } + add<1>(); + add(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([[1, '.!'], undefined]); +}); + +test('multiple deep calls optional types', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + function add(type?: ReceiveType) { + types.push(type); + add2(); + } + function add2(type?: ReceiveType) { + types.push(type); + } + add<1>(); + add(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([[1, '.!'], undefined, undefined, undefined]); +}); + +test('chained optional methods', () => { + const res = transpileAndRun({ + 'app': ` + const types: any[] = []; + class Http { + response(type?: ReceiveType) { + types.push(type); + return this; + } + } + const http = new Http; + http.response<1>().response(); + types; + ` + }); + + console.log(res); + expect(res).toEqual([[1, '.!'], undefined]); +}); diff --git a/packages/type-compiler/tests/utils.ts b/packages/type-compiler/tests/utils.ts new file mode 100644 index 000000000..b00f3345e --- /dev/null +++ b/packages/type-compiler/tests/utils.ts @@ -0,0 +1,118 @@ +import * as ts from 'typescript'; +import { createSourceFile, getPreEmitDiagnostics, ScriptTarget, TransformationContext } from 'typescript'; +import { createSystem, createVirtualCompilerHost, knownLibFilesForCompilerOptions } from '@typescript/vfs'; +import { ReflectionTransformer } from '../src/compiler'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { first } from '@deepkit/core'; + +const defaultLibLocation = __dirname + '/node_modules/typescript/lib/'; + +function fullPath(fileName: string): string { + return __dirname + '/' + fileName + (fileName.includes('.') ? '' : '.ts'); +} + +function readLibs(compilerOptions: ts.CompilerOptions, files: Map) { + const getLibSource = (name: string) => { + const lib = dirname(require.resolve('typescript')); + return readFileSync(join(lib, name), 'utf8'); + }; + const libs = knownLibFilesForCompilerOptions(compilerOptions, ts); + for (const lib of libs) { + if (lib.startsWith('lib.webworker.d.ts')) continue; //dom and webworker can not go together + + files.set(defaultLibLocation + lib, getLibSource(lib)); + } +} + +export function transform(files: Record, options: ts.CompilerOptions = {}): Record { + const compilerOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2016, + allowNonTsExtensions: true, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + experimentalDecorators: true, + esModuleInterop: true, + ...options + }; + + const fsMap = new Map(); + readLibs(compilerOptions, fsMap); + + const system = createSystem(fsMap); + + const host = createVirtualCompilerHost(system, compilerOptions, ts); + + const res: Record = {}; + + for (const [fileName, source] of Object.entries(files)) { + const sourceFile = createSourceFile(fullPath(fileName), source, compilerOptions.target || ScriptTarget.ES2018, true); + host.updateFile(sourceFile); + } + + for (const fileName of Object.keys(files)) { + const sourceFile = host.compilerHost.getSourceFile(fullPath(fileName), ScriptTarget.ES2022); + if (!sourceFile) continue; + const transform = ts.transform(sourceFile, [(context) => (node) => new ReflectionTransformer(context).forHost(host.compilerHost).withReflectionMode('always').transformSourceFile(node)]); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const code = printer.printNode(ts.EmitHint.SourceFile, transform.transformed[0], transform.transformed[0]); + res[fileName] = code; + } + + return res; +} + +/** + * The first entry in files is executed as main script + */ +export function transpileAndRun(files: Record, options: ts.CompilerOptions = {}): any { + const source = transpile(files); + console.log('transpiled', source); + const first = Object.keys(files)[0]; + + return eval(source[first]); +} + +export function transpile(files: Record, options: ts.CompilerOptions = {}): Record { + const compilerOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2015, + allowNonTsExtensions: true, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + experimentalDecorators: true, + esModuleInterop: true, + skipLibCheck: true, + ...options + }; + + const fsMap = new Map(); + readLibs(compilerOptions, fsMap); + compilerOptions.lib = [...fsMap.keys()]; + + for (const [fileName, source] of Object.entries(files)) { + fsMap.set(fullPath(fileName), source); + } + const system = createSystem(fsMap); + + const host = createVirtualCompilerHost(system, compilerOptions, ts); + host.compilerHost.getDefaultLibLocation = () => defaultLibLocation; + + const rootNames = Object.keys(files).map(fileName => fullPath(fileName)); + const program = ts.createProgram({ + rootNames: rootNames, + options: compilerOptions, + host: host.compilerHost, + }); + for (const d of getPreEmitDiagnostics(program)) { + console.log('diagnostics', d.file?.fileName, d.messageText, d.start, d.length); + } + const res: Record = {}; + + program.emit(undefined, (fileName, data) => { + res[fileName.slice(__dirname.length + 1).replace(/\.js$/, '')] = data; + }, undefined, undefined, { + before: [(context: TransformationContext) => new ReflectionTransformer(context).forHost(host.compilerHost).withReflectionMode('always')], + }); + + return res; +} diff --git a/packages/type-compiler/tsconfig.json b/packages/type-compiler/tsconfig.json index 7241c6d64..e413115a6 100644 --- a/packages/type-compiler/tsconfig.json +++ b/packages/type-compiler/tsconfig.json @@ -15,7 +15,6 @@ "declaration": true, "composite": true }, - "reflection": false, "include": [ "src", "tests", diff --git a/packages/type-spec/index.ts b/packages/type-spec/index.ts index 7749d14c7..be666d3ab 100644 --- a/packages/type-spec/index.ts +++ b/packages/type-spec/index.ts @@ -8,4 +8,4 @@ * You should have received a copy of the MIT License along with this program. */ -export * from './src/type'; +export * from './src/type.js'; diff --git a/packages/type/index.ts b/packages/type/index.ts index 2e4cea5d9..48e0ba6f1 100644 --- a/packages/type/index.ts +++ b/packages/type/index.ts @@ -8,30 +8,30 @@ * You should have received a copy of the MIT License along with this program. */ -export * from './src/core'; -export * from './src/changes'; -export * from './src/decorator'; -export * from './src/decorator-builder'; -export * from './src/reference'; -export * from './src/serializer'; -export * from './src/serializer-facade'; -export * from './src/typeguard'; -export * from './src/types'; -export * from './src/utils'; -export * from './src/validator'; -export * from './src/validators'; -export * from './src/snapshot'; -export * from './src/change-detector'; -export * from './src/path'; -export * from './src/type-serialization'; -export * from './src/registry'; -export * from './src/default'; -export * from './src/mixin'; +export * from './src/core.js'; +export * from './src/changes.js'; +export * from './src/decorator.js'; +export * from './src/decorator-builder.js'; +export * from './src/reference.js'; +export * from './src/serializer.js'; +export * from './src/serializer-facade.js'; +export * from './src/typeguard.js'; +export * from './src/types.js'; +export * from './src/utils.js'; +export * from './src/validator.js'; +export * from './src/validators.js'; +export * from './src/snapshot.js'; +export * from './src/change-detector.js'; +export * from './src/path.js'; +export * from './src/type-serialization.js'; +export * from './src/registry.js'; +export * from './src/default.js'; +export * from './src/mixin.js'; -export * from './src/reflection/type'; -export * from './src/reflection/processor'; -export * from './src/reflection/type'; -export * from './src/reflection/extends'; -export * from './src/reflection/reflection'; +export * from './src/reflection/type.js'; +export * from './src/reflection/processor.js'; +export * from './src/reflection/type.js'; +export * from './src/reflection/extends.js'; +export * from './src/reflection/reflection.js'; export { TypeNumberBrand } from '@deepkit/type-spec'; diff --git a/packages/type/package-lock.json b/packages/type/package-lock.json index 862470bfa..ec6b2bb08 100644 --- a/packages/type/package-lock.json +++ b/packages/type/package-lock.json @@ -1,13 +1,220 @@ { "name": "@deepkit/type", "version": "1.0.1-alpha.71", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "@deepkit/type", + "version": "1.0.1-alpha.71", + "license": "MIT", + "dependencies": { + "@types/uuid": "^8.3.0", + "buffer": "^5.2.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/lz-string": "^1.3.34", + "@types/node": "14.14.28", + "@typescript/vfs": "^1.3.5", + "lz-string": "^1.4.4" + }, + "peerDependencies": { + "@deepkit/core": "^1.0.1-alpha.13" + } + }, + "node_modules/@deepkit/core": { + "version": "1.0.1-alpha.65", + "resolved": "https://registry.npmjs.org/@deepkit/core/-/core-1.0.1-alpha.65.tgz", + "integrity": "sha512-L52r3uSGu+kEjsq/9vVoora2fIWDFi/wmwkGtDJjbpbW1oY02jGYy73ImM7jeKeTfFen+3bw6Fk+tTVH4EChRg==", + "peer": true, + "dependencies": { + "dot-prop": "^5.1.1", + "to-fast-properties": "^3.0.1" + } + }, + "node_modules/@types/lz-string": { + "version": "1.3.34", + "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz", + "integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.14.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", + "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, + "node_modules/@typescript/vfs": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.3.5.tgz", + "integrity": "sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "peer": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-3.0.1.tgz", + "integrity": "sha512-/wtNi1tW1F3nf0OL6AqVxGw9Tr1ET70InMhJuVxPwFdGqparF0nQ4UWGLf2DsoI2bFDtthlBnALncZpUzOnsUw==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + } + }, "dependencies": { - "@types/clone": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", - "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", + "@deepkit/core": { + "version": "1.0.1-alpha.65", + "resolved": "https://registry.npmjs.org/@deepkit/core/-/core-1.0.1-alpha.65.tgz", + "integrity": "sha512-L52r3uSGu+kEjsq/9vVoora2fIWDFi/wmwkGtDJjbpbW1oY02jGYy73ImM7jeKeTfFen+3bw6Fk+tTVH4EChRg==", + "peer": true, + "requires": { + "dot-prop": "^5.1.1", + "to-fast-properties": "^3.0.1" + } + }, + "@types/lz-string": { + "version": "1.3.34", + "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz", + "integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==", + "dev": true + }, + "@types/node": { + "version": "14.14.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", + "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==", "dev": true }, "@types/uuid": { @@ -15,10 +222,14 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, - "@types/validator": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.3.tgz", - "integrity": "sha512-DaOWN1zf7j+8nHhqXhIgNmS+ltAC53NXqGxYuBhWqWgqolRhddKzfZU814lkHQSTG0IUfQxU7Cg0gb8fFWo2mA==" + "@typescript/vfs": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.3.5.tgz", + "integrity": "sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } }, "base64-js": { "version": "1.5.1", @@ -34,31 +245,57 @@ "ieee754": "^1.1.13" } }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "peer": true, + "requires": { + "is-obj": "^2.0.0" + } }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "peer": true + }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "to-fast-properties": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-3.0.1.tgz", + "integrity": "sha512-/wtNi1tW1F3nf0OL6AqVxGw9Tr1ET70InMhJuVxPwFdGqparF0nQ4UWGLf2DsoI2bFDtthlBnALncZpUzOnsUw==", + "peer": true + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "validator": { - "version": "13.5.2", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.5.2.tgz", - "integrity": "sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ==" } } } diff --git a/packages/type/package.json b/packages/type/package.json index 9efa1f2c1..69d3fc25e 100644 --- a/packages/type/package.json +++ b/packages/type/package.json @@ -33,8 +33,11 @@ }, "devDependencies": { "@deepkit/core": "^1.0.1-alpha.65", + "@types/lz-string": "^1.3.34", "@deepkit/type-compiler": "^1.0.1-alpha.71", - "@types/node": "14.14.28" + "@typescript/vfs": "^1.3.5", + "@types/node": "14.14.28", + "lz-string": "^1.4.4" }, "jest": { "testEnvironment": "node", @@ -44,6 +47,9 @@ "testMatch": [ "**/tests/**/*.spec.ts" ], + "moduleNameMapper": { + "(.+)\\.js": "$1" + }, "globals": { "ts-jest": { "tsconfig": "/tsconfig.test.json" diff --git a/packages/type/src/change-detector.ts b/packages/type/src/change-detector.ts index 37ab9c42b..c9e51f314 100644 --- a/packages/type/src/change-detector.ts +++ b/packages/type/src/change-detector.ts @@ -9,11 +9,11 @@ */ import { CompilerContext, empty, toFastProperties } from '@deepkit/core'; -import { Changes, changeSetSymbol, ItemChanges } from './changes'; -import { getConverterForSnapshot } from './snapshot'; -import { ReflectionClass } from './reflection/reflection'; -import { ContainerAccessor, getIndexCheck, sortSignatures, TemplateRegistry, TemplateState } from './serializer'; -import { referenceAnnotation, ReflectionKind, Type, TypeIndexSignature } from './reflection/type'; +import { Changes, changeSetSymbol, ItemChanges } from './changes.js'; +import { getConverterForSnapshot } from './snapshot.js'; +import { ReflectionClass } from './reflection/reflection.js'; +import { ContainerAccessor, getIndexCheck, sortSignatures, TemplateRegistry, TemplateState } from './serializer.js'; +import { referenceAnnotation, ReflectionKind, Type, TypeIndexSignature } from './reflection/type.js'; function genericEqualArray(a: any[], b: any[]): boolean { if (a.length !== b.length) return false; @@ -271,7 +271,7 @@ function createJITChangeDetectorForSnapshot(schema: ReflectionClass, stateI const changeDetectorSymbol = Symbol('changeDetector'); -export function getChangeDetector(classSchema: ReflectionClass): (last: any, current: any, item: T) => ItemChanges | undefined { +export function getChangeDetector(classSchema: ReflectionClass): (last: any, current: any, item: T) => ItemChanges | undefined { const jit = classSchema.getJitContainer(); if (jit[changeDetectorSymbol]) return jit[changeDetectorSymbol]; @@ -281,7 +281,7 @@ export function getChangeDetector(classSchema: ReflectionClass): (last: an return jit[changeDetectorSymbol]; } -export function buildChanges(classSchema: ReflectionClass, lastSnapshot: any, item: T): Changes { +export function buildChanges(classSchema: ReflectionClass, lastSnapshot: any, item: T): Changes { const currentSnapshot = getConverterForSnapshot(classSchema)(item); const detector = getChangeDetector(classSchema); return detector(lastSnapshot, currentSnapshot, item) as Changes || new Changes(); diff --git a/packages/type/src/changes.ts b/packages/type/src/changes.ts index cdb3fe968..94215ef80 100644 --- a/packages/type/src/changes.ts +++ b/packages/type/src/changes.ts @@ -20,7 +20,7 @@ export interface ChangesInterface { $inc?: Partial>>; } -export class Changes { +export class Changes { $set?: Partial | T; $unset?: { [path: string]: number }; $inc?: Partial>>; @@ -96,7 +96,7 @@ export class Changes { } } -export class ItemChanges extends Changes { +export class ItemChanges extends Changes { constructor( changes: ChangesInterface = {}, protected item: T @@ -122,7 +122,7 @@ export class ItemChanges extends Changes { export const changeSetSymbol = Symbol('changeSet'); -export class AtomicChangeInstance { +export class AtomicChangeInstance { public readonly changeSet: Changes = new Changes(); constructor(protected object: any) { @@ -137,6 +137,6 @@ export class AtomicChangeInstance { } } -export function atomicChange(object: T) { +export function atomicChange(object: T) { return new AtomicChangeInstance(object); } diff --git a/packages/type/src/decorator-builder.ts b/packages/type/src/decorator-builder.ts index 7e66b6b8b..b757a515e 100644 --- a/packages/type/src/decorator-builder.ts +++ b/packages/type/src/decorator-builder.ts @@ -22,14 +22,14 @@ export type FluidDecorator = { export function createFluidDecorator | APIProperty, D extends Function> ( api: API, - modifier: { name: string, args?: any }[], + modifier: { name: string, args?: any, Ω?: any }[], collapse: (modifier: { name: string, args?: any }[], target: any, property?: string, parameterIndexOrDescriptor?: any) => void, returnCollapse: boolean = false, fluidFunctionSymbol?: symbol ): FluidDecorator, D> { const fn = function (target: object, property?: string, parameterIndexOrDescriptor?: any) { const res = collapse(modifier, target, property, parameterIndexOrDescriptor); - if (returnCollapse) return res; + if (returnCollapse || target === Object) return res; }; Object.defineProperty(fn, 'name', { value: undefined }); Object.defineProperty(fn, '_data', { @@ -65,8 +65,8 @@ export function createFluidDecorator | APIProperty { - return createFluidDecorator(api, [...modifier, { name, args }], collapse, returnCollapse, fluidFunctionSymbol); + value: function fn(...args: any[]) { + return createFluidDecorator(api, [...modifier, { name, args, Ω: (fn as any).Ω }], collapse, returnCollapse, fluidFunctionSymbol); } }); } @@ -118,11 +118,12 @@ export function mergeDecorator(...args: T): Merge void, ): any { const fn = function (target: object, property?: string, parameterIndexOrDescriptor?: any) { - collapse(modifier, target, property, parameterIndexOrDescriptor); + const res = collapse(modifier, target, property, parameterIndexOrDescriptor); + if (target === Object) return res; }; Object.defineProperty(fn, 'name', { value: undefined }); @@ -144,10 +145,8 @@ export function mergeDecorator(...args: T): Merge { - return (...args: any[]) => { - return fluid([...modifier, { name, args }], collapse); - }; + value: function fn(...args: any[]) { + return fluid([...modifier, { name, args, Ω: (fn as any).Ω }], collapse); } }); } @@ -155,16 +154,18 @@ export function mergeDecorator(...args: T): Merge(...args: T): Merge(...args: T): Merge, T = Extra ): ClassDecoratorResult { const map = new Map>(); - function collapse(modifier: { name: string, args?: any }[], target: ClassType) { + function collapse(modifier: { name: string, args?: any, Ω?: any }[], target: ClassType): any { const api: ClassApiTypeInterface = map.get(target) ?? new apiType(target); for (const fn of modifier) { if (fn.args) { - (api as any)[fn.name].bind(api)(...fn.args); + const f = (api as any)[fn.name]; + f.Ω = fn.Ω; + f.call(api, ...fn.args); } else { //just call the getter (api as any)[fn.name]; @@ -224,6 +229,7 @@ export function createClassDecoratorContext, T = Extra if (api.onDecorator) api.onDecorator(target); map.set(target, api); + if (target === Object) return api.t; } const fn = createFluidDecorator(apiType, [], collapse); @@ -260,10 +266,10 @@ export function createPropertyDecoratorContext>( ): PropertyDecoratorResult { const targetMap = new Map>>(); - function collapse(modifier: { name: string, args?: any }[], target: object, property?: string, parameterIndexOrDescriptor?: any) { + function collapse(modifier: { name: string, args?: any, Ω?: any }[], target: object, property?: string, parameterIndexOrDescriptor?: any): any { if (property === undefined && parameterIndexOrDescriptor === undefined) throw new Error('Property decorators can only be used on class properties'); - target = (target as any)['constructor']; //property decorators get the prototype instead of the class. + target = target === Object ? target : (target as any)['constructor']; //property decorators get the prototype instead of the class. let map = targetMap.get(target); if (!map) { map = new Map(); @@ -275,7 +281,9 @@ export function createPropertyDecoratorContext>( for (const fn of modifier) { if (fn.args) { - (api as any)[fn.name].bind(api)(...fn.args); + const f = (api as any)[fn.name]; + f.Ω = fn.Ω; + f.call(api, ...fn.args); } else { //just call the getter (api as any)[fn.name]; @@ -285,6 +293,7 @@ export function createPropertyDecoratorContext>( if (api.onDecorator) api.onDecorator(target as ClassType, property, ('number' === typeof parameterIndexOrDescriptor ? parameterIndexOrDescriptor : undefined)); map.set(index, api); + if (target === Object) return api.t; } const fn = createFluidDecorator(apiType, [], collapse); @@ -322,12 +331,14 @@ export type FreeDecoratorResult> = FreeFluidDecorator< export function createFreeDecoratorContext, T = ExtractApiDataType>( apiType: API ): FreeDecoratorResult { - function collapse(modifier: { name: string, args?: any }[], target?: any, property?: string, parameterIndexOrDescriptor?: any) { + function collapse(modifier: { name: string, args?: any, Ω?: any }[], target?: any, property?: string, parameterIndexOrDescriptor?: any) { const api = new apiType; for (const fn of modifier) { if (fn.args) { - (api as any)[fn.name].bind(api)(...fn.args); + const f = (api as any)[fn.name]; + f.Ω = fn.Ω; + f.call(api, ...fn.args); } else { //just call the getter (api as any)[fn.name]; diff --git a/packages/type/src/decorator.ts b/packages/type/src/decorator.ts index e1299ce55..3080bcdc7 100644 --- a/packages/type/src/decorator.ts +++ b/packages/type/src/decorator.ts @@ -8,12 +8,12 @@ * You should have received a copy of the MIT License along with this program. */ -import { ClassDecoratorResult, createClassDecoratorContext, createPropertyDecoratorContext } from './decorator-builder'; -import { EntityData, ReceiveType, SerializerFn, TData } from './reflection/reflection'; +import { ClassDecoratorResult, createClassDecoratorContext, createPropertyDecoratorContext } from './decorator-builder.js'; +import { EntityData, ReceiveType, SerializerFn, TData } from './reflection/reflection.js'; import { ClassType, isArray } from '@deepkit/core'; -import { IndexOptions } from './reflection/type'; -import type { ValidateFunction } from './validator'; -import { typeSettings } from './core'; +import { IndexOptions } from './reflection/type.js'; +import type { ValidateFunction } from './validator.js'; +import { typeSettings } from './core.js'; class TDecorator { t = new TData(); diff --git a/packages/type/src/default.ts b/packages/type/src/default.ts index eaa9d715d..4671b16a3 100644 --- a/packages/type/src/default.ts +++ b/packages/type/src/default.ts @@ -1,4 +1,4 @@ -import { binaryTypes, ReflectionKind, resolveTypeMembers, Type } from './reflection/type'; +import { binaryTypes, ReflectionKind, resolveTypeMembers, Type } from './reflection/type.js'; /** * Returns a sensible default value for a given type. diff --git a/packages/type/src/inheritance.ts b/packages/type/src/inheritance.ts index 1e49f52b8..495197657 100644 --- a/packages/type/src/inheritance.ts +++ b/packages/type/src/inheritance.ts @@ -31,8 +31,8 @@ // return discriminatorFound ? discriminatorFound.property.name : undefined; // } -import { ReflectionClass } from './reflection/reflection'; -import { ReflectionKind } from './reflection/type'; +import { ReflectionClass } from './reflection/reflection.js'; +import { ReflectionKind } from './reflection/type.js'; export function findCommonLiteral(reflectionClasses: ReflectionClass[]): string | undefined { const candidates: { [name: string]: { found: number, values: any[], schemas: ReflectionClass[] } } = {}; diff --git a/packages/type/src/mixin.ts b/packages/type/src/mixin.ts index e118bda95..072b66259 100644 --- a/packages/type/src/mixin.ts +++ b/packages/type/src/mixin.ts @@ -1,7 +1,7 @@ import { ExtractClassType } from '@deepkit/core'; import { ClassType } from '@deepkit/core'; import { AbstractClassType } from '@deepkit/core'; -import { ReflectionClass } from './reflection/reflection'; +import { ReflectionClass } from './reflection/reflection.js'; type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; diff --git a/packages/type/src/path.ts b/packages/type/src/path.ts index e838312b6..e34205765 100644 --- a/packages/type/src/path.ts +++ b/packages/type/src/path.ts @@ -1,7 +1,7 @@ -import { getTypeJitContainer, ReflectionKind, Type } from './reflection/type'; +import { getTypeJitContainer, ReflectionKind, Type } from './reflection/type.js'; import { CompilerContext, toFastProperties } from '@deepkit/core'; -import { JitStack } from './serializer'; -import { ReceiveType, resolveReceiveType } from './reflection/reflection'; +import { ReceiveType, resolveReceiveType } from './reflection/reflection.js'; +import { JitStack } from './serializer.js'; export type Resolver = (path: string) => Type | undefined; diff --git a/packages/type/src/reference.ts b/packages/type/src/reference.ts index 9103d47cf..2d910626a 100644 --- a/packages/type/src/reference.ts +++ b/packages/type/src/reference.ts @@ -9,9 +9,9 @@ */ import { ClassType, isObject } from '@deepkit/core'; -import { ReflectionClass, reflectionClassSymbol } from './reflection/reflection'; -import { typeSettings, UnpopulatedCheck, unpopulatedSymbol } from './core'; -import { ReflectionKind, Type } from './reflection/type'; +import { ReflectionClass, reflectionClassSymbol } from './reflection/reflection.js'; +import { typeSettings, UnpopulatedCheck, unpopulatedSymbol } from './core.js'; +import { ReflectionKind, Type } from './reflection/type.js'; export function isReferenceInstance(obj: any): boolean { return isObject(obj) && referenceSymbol in obj; @@ -66,7 +66,7 @@ export function createReference(type: ClassType | Type | ReflectionClass ClassType | Object) | string | number | boolean | bigint; @@ -316,6 +316,14 @@ export class Processor { reflect(object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = [], options: ReflectOptions = {}): Type { const packed: Packed | undefined = isPack(object) ? object : object.__type; if (!packed) { + if (isFunction(object) && object.length === 0) { + //functions without any type annotations do not have the overhead of an assigned __type + return { + kind: ReflectionKind.function, + function: object, name: object.name, + parameters: [], return: { kind: ReflectionKind.any } + }; + } throw new Error(`No valid runtime type for ${stringifyValueWithType(object)} given. Is @deepkit/type-compiler correctly installed? Execute deepkit-type-install to check`); } @@ -323,9 +331,9 @@ export class Processor { if (isClass(inputs[i])) inputs[i] = resolveRuntimeType(inputs[i]); } - // //this checks if there is an active program still running for given packed. if so, issue a new reference. - // //this reference is changed (its content only via Object.assign(reference, computedValues)) once the program finished. - // //this is independent of reuseCache since it's the cache for the current 'run', not a global cache + //this checks if there is an active program still running for given packed. if so, issue a new reference. + //this reference is changed (its content only via Object.assign(reference, computedValues)) once the program finished. + //this is independent of reuseCache since it's the cache for the current 'run', not a global cache const found = findExistingProgram(this.program, object, inputs); if (found) { return createRef(found); @@ -399,7 +407,7 @@ export class Processor { for (; program.program < program.end; program.program++) { const op = program.ops[program.program]; - // process.stdout.write(`[${program.depth}:${program.frame.index}] step ${program.program} ${ReflectionOp[op]}\n`); + // process.stdout.write(`[${program.depth}:${program.frame.index}] step ${program.program} ${RuntimeReflectionOp[op]}\n`); switch (op) { case ReflectionOp.string: this.pushType({ kind: ReflectionKind.string }); @@ -671,7 +679,7 @@ export class Processor { if (type === undefined) { //generic not instantiated - program.typeParameters.push({ kind: ReflectionKind.any }); + program.typeParameters.push({ kind: ReflectionKind.any, typeParameter: true } as any); this.pushType({ kind: ReflectionKind.typeParameter, name: program.stack[nameRef] as string }); } else { program.typeParameters.push(type as Type); @@ -929,7 +937,7 @@ export class Processor { break; } case ReflectionOp.var: { - this.push({ kind: ReflectionKind.unknown }); + this.push({ kind: ReflectionKind.unknown, var: true }); program.frame.variables++; break; } @@ -1285,6 +1293,8 @@ export class Processor { this.push(union); } else if (type.kind === ReflectionKind.any) { this.push({ kind: ReflectionKind.union, types: [{ kind: ReflectionKind.string }, { kind: ReflectionKind.number }, { kind: ReflectionKind.symbol }] }); + } else { + this.push({ kind: ReflectionKind.never }); } } @@ -1420,6 +1430,7 @@ export class Processor { protected pop(): RuntimeStackEntry { if (this.program.stackPointer < 0) throw new Error('Stack empty'); + // return this.program.stack.pop()!; return this.program.stack[this.program.stackPointer--]; } diff --git a/packages/type/src/reflection/reflection.ts b/packages/type/src/reflection/reflection.ts index a92889c58..abdad1644 100644 --- a/packages/type/src/reflection/reflection.ts +++ b/packages/type/src/reflection/reflection.ts @@ -49,14 +49,14 @@ import { TypeProperty, TypePropertySignature, TypeTemplateLiteral -} from './type'; +} from './type.js'; import { AbstractClassType, arrayRemoveItem, ClassType, getClassName, isArray, isClass, stringifyValueWithType } from '@deepkit/core'; -import { Packed, resolvePacked, resolveRuntimeType } from './processor'; -import { NoTypeReceived } from '../utils'; -import { findCommonLiteral } from '../inheritance'; -import type { ValidateFunction } from '../validator'; -import { isWithDeferredDecorators } from '../decorator'; -import { SerializedTypes, serializeType } from '../type-serialization'; +import { Packed, resolvePacked, resolveRuntimeType } from './processor.js'; +import { NoTypeReceived } from '../utils.js'; +import { findCommonLiteral } from '../inheritance.js'; +import type { ValidateFunction } from '../validator.js'; +import { isWithDeferredDecorators } from '../decorator.js'; +import { SerializedTypes, serializeType } from '../type-serialization.js'; /** * Receives the runtime type of template argument. @@ -245,7 +245,7 @@ export class ReflectionParameter { constructor( public readonly parameter: TypeParameter, - public readonly reflectionMethod: ReflectionMethod, + public readonly reflectionFunction: ReflectionMethod | ReflectionFunction, ) { this.type = this.parameter.type; } @@ -289,8 +289,8 @@ export class ReflectionParameter { applyDecorator(t: TData) { if (t.type) { this.type = resolveReceiveType(t.type); - if (this.getVisibility() !== undefined) { - this.reflectionMethod.reflectionClass.getProperty(this.getName())!.setType(this.type); + if (this.getVisibility() !== undefined && this.reflectionFunction instanceof ReflectionMethod) { + this.reflectionFunction.reflectionClass.getProperty(this.getName())!.setType(this.type); } } } @@ -312,40 +312,30 @@ export class ReflectionParameter { } } -export class ReflectionMethod { +export class ReflectionFunction { parameters: ReflectionParameter[] = []; - /** - * Whether this method acts as validator. - */ - validator: boolean = false; - constructor( - public method: TypeMethod | TypeMethodSignature, - public reflectionClass: ReflectionClass, + public readonly type: TypeMethod | TypeMethodSignature | TypeFunction, ) { - this.setType(method); - } - - setType(method: TypeMethod | TypeMethodSignature) { - this.method = method; - this.parameters = []; - for (const p of this.method.parameters) { + for (const p of this.type.parameters) { this.parameters.push(new ReflectionParameter(p, this)); } } - applyDecorator(data: TData) { - this.validator = data.validator; - if (this.validator) { - this.reflectionClass.validationMethod = this.getName(); + static from(fn: Function): ReflectionFunction { + //todo: cache it + + if (!('__type' in fn)) { + //functions without any types have no __type attached + return new ReflectionFunction({ kind: ReflectionKind.function, function: fn, return: { kind: ReflectionKind.any }, parameters: [] }); } - } - clone(reflectionClass?: ReflectionClass, method?: TypeMethod | TypeMethodSignature): ReflectionMethod { - const c = new ReflectionMethod(method || this.method, reflectionClass || this.reflectionClass); - //todo, clone parameter - return c; + const type = reflect(fn); + if (type.kind !== ReflectionKind.function) { + throw new Error(`Given object is not a function ${fn}`); + } + return new ReflectionFunction(type); } getParameterNames(): (string)[] { @@ -380,15 +370,11 @@ export class ReflectionMethod { } getReturnType(): Type { - return this.method.return; - } - - isOptional(): boolean { - return this.method.optional === true; + return this.type.return; } getName(): number | string | symbol { - return this.method.name; + return this.type.name || 'anonymous'; } get name(): string { @@ -396,54 +382,42 @@ export class ReflectionMethod { } } -export class ReflectionFunction { +export class ReflectionMethod extends ReflectionFunction { + /** + * Whether this method acts as validator. + */ + validator: boolean = false; + constructor( - public readonly type: TypeFunction, + public type: TypeMethod | TypeMethodSignature, + public reflectionClass: ReflectionClass, ) { + super(type); } - static from(fn: Function): ReflectionFunction { - //todo: cache it - - if (!('__type' in fn)) { - //functions without any types have no __type attached - return new ReflectionFunction({ kind: ReflectionKind.function, function: fn, return: { kind: ReflectionKind.any }, parameters: [] }); - } - - const type = reflect(fn); - if (type.kind !== ReflectionKind.function) { - throw new Error(`Given object is not a function ${fn}`); + setType(method: TypeMethod | TypeMethodSignature) { + this.type = method; + this.parameters = []; + for (const p of this.type.parameters) { + this.parameters.push(new ReflectionParameter(p, this)); } - return new ReflectionFunction(type); } - getParameters(): TypeParameter[] { - return this.type.parameters; - } - - getParameterNames(): string[] { - return this.type.parameters.map(v => v.name); - } - - getParameterType(name: string): Type | undefined { - const parameter = this.getParameter(name); - if (parameter) return parameter.type; - return; - } - - getParameter(name: string): TypeParameter | undefined { - for (const parameter of this.type.parameters) { - if (parameter.name === name) return parameter; + applyDecorator(data: TData) { + this.validator = data.validator; + if (this.validator) { + this.reflectionClass.validationMethod = this.getName(); } - return; } - getReturnType(): Type { - return this.type.return; + clone(reflectionClass?: ReflectionClass, method?: TypeMethod | TypeMethodSignature): ReflectionMethod { + const c = new ReflectionMethod(method || this.type, reflectionClass || this.reflectionClass); + //todo, clone parameter + return c; } - getName(): number | string | symbol | undefined { - return this.type.name; + isOptional(): boolean { + return this.type.optional === true; } } @@ -1190,7 +1164,7 @@ export class ReflectionClass { if (classTypeIn instanceof ReflectionClass) return classTypeIn; if (isType(classTypeIn)) { - if (classTypeIn.kind === ReflectionKind.objectLiteral) { + if (classTypeIn.kind === ReflectionKind.objectLiteral || (classTypeIn.kind === ReflectionKind.class && classTypeIn.typeArguments)) { const jit = getTypeJitContainer(classTypeIn); if (jit.reflectionClass) return jit.reflectionClass; return jit.reflectionClass = new ReflectionClass(classTypeIn); diff --git a/packages/type/src/reflection/type.ts b/packages/type/src/reflection/type.ts index 76d799132..5fc972597 100644 --- a/packages/type/src/reflection/type.ts +++ b/packages/type/src/reflection/type.ts @@ -10,8 +10,8 @@ import { AbstractClassType, ClassType, getClassName, getParentClass, indent, isArray } from '@deepkit/core'; import { TypeNumberBrand } from '@deepkit/type-spec'; -import { getProperty, ReceiveType, reflect, ReflectionClass, resolveReceiveType, toSignature } from './reflection'; -import { isExtendable } from './extends'; +import { getProperty, ReceiveType, reflect, ReflectionClass, resolveReceiveType, toSignature } from './reflection.js'; +import { isExtendable } from './extends.js'; import { isClass } from '@deepkit/core'; export enum ReflectionVisibility { @@ -20,7 +20,7 @@ export enum ReflectionVisibility { private, } -export const enum ReflectionKind { +export enum ReflectionKind { never, any, unknown, @@ -1302,6 +1302,10 @@ export class AnnotationDefinition { annotations[this.symbol].push(data); } + reset(annotations: Annotations) { + annotations[this.symbol] = undefined; + } + registerType(type: TType, data: T): TType { type.annotations ||= {}; this.register(type.annotations, data); @@ -1594,6 +1598,21 @@ export type Group = { __meta?: ['group', never & Name] }; export type Excluded = { __meta?: ['excluded', never & Name] }; export type Data = { __meta?: ['data', never & Name, never & Value] }; +/** + * Resets an already set decorator to undefined. + * + * The required Name is the name of the type decorator (its first tuple entry). + * + * ```typescript + * type Password = string & MinLength<6> & Excluded; + * + * interface UserCreationPayload { + * password: Password & ResetDecorator<'excluded'> + * } + * ``` + */ +export type ResetDecorator = { __meta?: ['reset', Name] }; + export type IndexOptions = { name?: string; //index size. Necessary for blob/longtext, etc. @@ -1776,6 +1795,25 @@ export const typeDecorators: TypeDecorator[] = [ excludedAnnotation.register(annotations, nameType.type.literal); return true; } + case 'reset': { + const name = typeToObject(meta.type.types[1].type); + if ('string' !== typeof name) return false; + const map: { [name: string]: AnnotationDefinition } = { + excluded: excludedAnnotation, + database: databaseAnnotation, + index: indexAnnotation, + data: dataAnnotation, + group: groupAnnotation, + embedded: excludedAnnotation, + mapName: mapNameAnnotation, + reference: referenceAnnotation, + backReference: backReferenceAnnotation, + validator: validationAnnotation, + }; + const annotation = map[name] || metaAnnotation; + annotation.reset(annotations); + return true; + } case 'data': { const nameType = meta.type.types[1]; if (!nameType || nameType.type.kind !== ReflectionKind.literal || 'string' !== typeof nameType.type.literal) return false; diff --git a/packages/type/src/registry.ts b/packages/type/src/registry.ts index d1e8b3123..772f40ba9 100644 --- a/packages/type/src/registry.ts +++ b/packages/type/src/registry.ts @@ -1,5 +1,5 @@ import { ClassType, isArray, isFunction } from '@deepkit/core'; -import { binaryTypes, ReflectionKind, Type } from './reflection/type'; +import { binaryTypes, ReflectionKind, Type } from './reflection/type.js'; interface RegistryDecorator { predicate: (type: Type) => boolean, diff --git a/packages/type/src/serializer-facade.ts b/packages/type/src/serializer-facade.ts index a3c6274b0..09cbaa9e3 100644 --- a/packages/type/src/serializer-facade.ts +++ b/packages/type/src/serializer-facade.ts @@ -1,9 +1,9 @@ import { getClassTypeFromInstance } from '@deepkit/core'; -import { ReceiveType, resolveReceiveType } from './reflection/reflection'; -import { getSerializeFunction, NamingStrategy, SerializationOptions, serializer, Serializer } from './serializer'; -import { JSONPartial, JSONSingle } from './utils'; -import { typeInfer } from './reflection/processor'; -import { assert } from './typeguard'; +import { ReceiveType, resolveReceiveType } from './reflection/reflection.js'; +import { getSerializeFunction, NamingStrategy, SerializationOptions, serializer, Serializer } from './serializer.js'; +import { JSONPartial, JSONSingle } from './utils.js'; +import { typeInfer } from './reflection/processor.js'; +import { assert } from './typeguard.js'; /** * Casts/coerces a given data structure to the target data type and validates all attached validators. diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index e22e22321..8d124132e 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -52,17 +52,15 @@ import { TypeTuple, TypeUnion, validationAnnotation -} from './reflection/type'; +} from './reflection/type.js'; import { TypeNumberBrand } from '@deepkit/type-spec'; -import { hasCircularReference, ReflectionClass, ReflectionProperty } from './reflection/reflection'; -import { extendTemplateLiteral, isExtendable } from './reflection/extends'; -import { resolveRuntimeType } from './reflection/processor'; -import { createReference, isReferenceHydrated, isReferenceInstance } from './reference'; -import { validate, ValidationError, ValidationErrorItem } from './validator'; -import { validators } from './validators'; -import { arrayBufferToBase64, base64ToArrayBuffer, base64ToTypedArray, typedArrayToBase64, typeSettings, UnpopulatedCheck, unpopulatedSymbol } from './core'; - -if (!binaryTypes) throw new Error('WAT'); +import { hasCircularReference, ReflectionClass, ReflectionProperty } from './reflection/reflection.js'; +import { extendTemplateLiteral, isExtendable } from './reflection/extends.js'; +import { resolveRuntimeType } from './reflection/processor.js'; +import { createReference, isReferenceHydrated, isReferenceInstance } from './reference.js'; +import { validate, ValidationError, ValidationErrorItem } from './validator.js'; +import { validators } from './validators.js'; +import { arrayBufferToBase64, base64ToArrayBuffer, base64ToTypedArray, typedArrayToBase64, typeSettings, UnpopulatedCheck, unpopulatedSymbol } from './core.js'; /** * Make sure to change the id when a custom naming strategy is implemented, since caches are based on it. diff --git a/packages/type/src/snapshot.ts b/packages/type/src/snapshot.ts index 8382ceecd..3046eae08 100644 --- a/packages/type/src/snapshot.ts +++ b/packages/type/src/snapshot.ts @@ -9,10 +9,10 @@ */ import { CompilerContext, isObject, toFastProperties } from '@deepkit/core'; -import { typeSettings, UnpopulatedCheck } from './core'; -import { ReflectionClass, ReflectionProperty } from './reflection/reflection'; -import { ContainerAccessor, executeTemplates, noopTemplate, serializer, Serializer, TemplateRegistry, TemplateState } from './serializer'; -import { ReflectionKind } from './reflection/type'; +import { typeSettings, UnpopulatedCheck } from './core.js'; +import { ReflectionClass, ReflectionProperty } from './reflection/reflection.js'; +import { ContainerAccessor, executeTemplates, noopTemplate, serializer, Serializer, TemplateRegistry, TemplateState } from './serializer.js'; +import { ReflectionKind } from './reflection/type.js'; function createJITConverterForSnapshot( schema: ReflectionClass, diff --git a/packages/type/src/type-serialization.ts b/packages/type/src/type-serialization.ts index 99f973dac..3c638f3be 100644 --- a/packages/type/src/type-serialization.ts +++ b/packages/type/src/type-serialization.ts @@ -21,11 +21,11 @@ import { TypeRest, TypeTuple, TypeTupleMember -} from './reflection/type'; +} from './reflection/type.js'; import { getClassName, getParentClass } from '@deepkit/core'; -import { reflect, ReflectionClass, typeOf } from './reflection/reflection'; -import { typeSettings } from './core'; -import { regExpFromString } from './utils'; +import { reflect, ReflectionClass, typeOf } from './reflection/reflection.js'; +import { typeSettings } from './core.js'; +import { regExpFromString } from './utils.js'; export interface SerializedTypeAnnotations { entityOptions?: EntityOptions; diff --git a/packages/type/src/typeguard.ts b/packages/type/src/typeguard.ts index 599413a64..6a688f1bd 100644 --- a/packages/type/src/typeguard.ts +++ b/packages/type/src/typeguard.ts @@ -1,8 +1,8 @@ -import { ReceiveType, resolveReceiveType } from './reflection/reflection'; -import { createTypeGuardFunction, Guard, serializer, Serializer } from './serializer'; -import { NoTypeReceived } from './utils'; -import { ValidationError, ValidationErrorItem } from './validator'; -import { getTypeJitContainer } from './reflection/type'; +import { ReceiveType, resolveReceiveType } from './reflection/reflection.js'; +import { createTypeGuardFunction, Guard, serializer, Serializer } from './serializer.js'; +import { NoTypeReceived } from './utils.js'; +import { ValidationError, ValidationErrorItem } from './validator.js'; +import { getTypeJitContainer } from './reflection/type.js'; /** * ```typescript diff --git a/packages/type/src/types.ts b/packages/type/src/types.ts index 16e61a9b1..b06431aac 100644 --- a/packages/type/src/types.ts +++ b/packages/type/src/types.ts @@ -1,4 +1,4 @@ -import { AutoIncrement, PrimaryKey } from './reflection/type'; -import { Positive } from './validator'; +import { AutoIncrement, PrimaryKey } from './reflection/type.js'; +import { Positive } from './validator.js'; export type AutoId = number & PrimaryKey & AutoIncrement & Positive; diff --git a/packages/type/src/validator.ts b/packages/type/src/validator.ts index 249952c66..c56ef67d7 100644 --- a/packages/type/src/validator.ts +++ b/packages/type/src/validator.ts @@ -1,9 +1,9 @@ -import { ReceiveType, resolveReceiveType } from './reflection/reflection'; -import { getValidatorFunction, is } from './typeguard'; +import { ReceiveType } from './reflection/reflection.js'; +import { getValidatorFunction, is } from './typeguard.js'; import { CustomError } from '@deepkit/core'; -import { stringifyType, Type } from './reflection/type'; -import { entity } from './decorator'; -import { serializer, Serializer } from './serializer'; +import { stringifyType, Type } from './reflection/type.js'; +import { entity } from './decorator.js'; +import { serializer, Serializer } from './serializer.js'; export type ValidatorMeta = { __meta?: ['validator', Name, Args] } diff --git a/packages/type/src/validators.ts b/packages/type/src/validators.ts index eddb03f3e..dff9eb425 100644 --- a/packages/type/src/validators.ts +++ b/packages/type/src/validators.ts @@ -1,6 +1,6 @@ import { isArray } from '@deepkit/core'; -import { ValidatorError } from './validator'; -import { TypeLiteral } from './reflection/type'; +import { ValidatorError } from './validator.js'; +import { TypeLiteral } from './reflection/type.js'; export const validators: { [name in string]?: (...args: any[]) => (value: any) => ValidatorError | undefined } = { pattern(type: TypeLiteral) { diff --git a/packages/type/tests/compiler.spec.ts b/packages/type/tests/compiler.spec.ts index 0e42869ef..6335a2c33 100644 --- a/packages/type/tests/compiler.spec.ts +++ b/packages/type/tests/compiler.spec.ts @@ -1,17 +1,7 @@ /** @reflection never */ import { describe, expect, test } from '@jest/globals'; -import { - CompilerOptions, - createCompilerHost, - createProgram, - createSourceFile, - ModuleKind, - ScriptKind, - ScriptTarget, - SourceFile, - TransformationContext, - transpileModule -} from 'typescript'; +import * as ts from 'typescript'; +import { getPreEmitDiagnostics, ModuleKind, ScriptTarget, TransformationContext, transpileModule } from 'typescript'; import { DeclarationTransformer, ReflectionTransformer, transformer } from '@deepkit/type-compiler'; import { reflect, reflect as reflect2, ReflectionClass, removeTypeName, typeOf as typeOf2 } from '../src/reflection/reflection'; import { @@ -33,26 +23,46 @@ import { ReflectionOp } from '@deepkit/type-spec'; import { ClassType, isObject } from '@deepkit/core'; import { pack, resolveRuntimeType, typeInfer } from '../src/reflection/processor'; import { expectEqualType } from './utils'; +import { createSystem, createVirtualCompilerHost, knownLibFilesForCompilerOptions } from '@typescript/vfs'; +import { dirname, join } from 'path'; +import { readFileSync } from 'fs'; Error.stackTraceLimit = 200; -const options: CompilerOptions = { - experimentalDecorators: true, - module: ModuleKind.ES2020, - declaration: true, - transpileOnly: true, - target: ScriptTarget.ES2020, - reflection: true, -}; -/** - * @reflection never - */ -function transpile(source: T, optionsToUse: CompilerOptions = options): T extends string ? string : Record { - if ('string' === typeof source) { - return transpileModule(source, { +const defaultLibLocation = __dirname + '/node_modules/typescript/lib/'; + +function readLibs(compilerOptions: ts.CompilerOptions, files: Map) { + const getLibSource = (name: string) => { + const lib = dirname(require.resolve('typescript')); + return readFileSync(join(lib, name), 'utf8'); + }; + const libs = knownLibFilesForCompilerOptions(compilerOptions, ts); + for (const lib of libs) { + if (lib.startsWith('lib.webworker.d.ts')) continue; //dom and webworker can not go together + + files.set(defaultLibLocation + lib, getLibSource(lib)); + } +} + +export function transpile>(files: T, options: ts.CompilerOptions = {}): T extends string ? string : Record { + const compilerOptions: ts.CompilerOptions = { + target: ScriptTarget.ES2020, + allowNonTsExtensions: true, + module: ModuleKind.ES2020, + declaration: true, + transpileOnly: true, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + experimentalDecorators: true, + esModuleInterop: true, + skipLibCheck: true, + ...options + }; + + if ('string' === typeof files) { + return transpileModule(files, { fileName: __dirname + '/module.ts', - compilerOptions: optionsToUse, + compilerOptions, transformers: { before: [(context: TransformationContext) => new ReflectionTransformer(context).withReflectionMode('always')], afterDeclarations: [(context: TransformationContext) => new DeclarationTransformer(context).withReflectionMode('always')], @@ -60,37 +70,36 @@ function transpile(source: T, opt }).outputText as any; } - const files: { [path: string]: SourceFile } = {}; - const appPath = __dirname + '/app.ts'; - if ('string' === typeof source) { - files[appPath] = createSourceFile(appPath, source, ScriptTarget.ES2020, true, ScriptKind.TS); - } else { - for (const [file, src] of Object.entries(source)) { - const filePath = file === 'app.ts' ? appPath : __dirname + '/' + file + '.ts'; - files[filePath] = createSourceFile(filePath, src, ScriptTarget.ES2020, true, ScriptKind.TS); - } + const fsMap = new Map(); + readLibs(compilerOptions, fsMap); + compilerOptions.lib = [...fsMap.keys()]; + + for (const [fileName, source] of Object.entries(files)) { + fsMap.set(__dirname + '/' + fileName + '.ts', source); } + const system = createSystem(fsMap); - const result: Record = {}; - const host = createCompilerHost(optionsToUse); - host.writeFile = (fileName, data, writeByteOrderMark, onError, sourceFiles) => { - result[fileName.slice(__dirname.length + 1)] = data; - }; + const host = createVirtualCompilerHost(system, compilerOptions, ts); + host.compilerHost.getDefaultLibLocation = () => defaultLibLocation; - const ori = { ...host }; - host.getSourceFile = (fileName: string, languageVersion: ScriptTarget) => { - return files[fileName] || ori.getSourceFile(fileName, languageVersion); - }; - host.fileExists = (fileName: string) => { - return !!files[fileName] || ori.fileExists(fileName); - }; + const rootNames = Object.keys(files).map(fileName => __dirname + '/' + fileName + '.ts'); + const program = ts.createProgram({ + rootNames: rootNames, + options: compilerOptions, + host: host.compilerHost, + }); + for (const d of getPreEmitDiagnostics(program)) { + console.log('diagnostics', d.file?.fileName, d.messageText, d.start, d.length); + } + const res: Record = {}; - const program = createProgram(Object.keys(files), optionsToUse, host); - program.emit(undefined, undefined, undefined, undefined, { - before: [(context: TransformationContext) => new ReflectionTransformer(context).withReflectionMode('always')], - afterDeclarations: [(context: TransformationContext) => new DeclarationTransformer(context).withReflectionMode('always')], + program.emit(undefined, (fileName, data) => { + res[fileName.slice(__dirname.length + 1)] = data; + }, undefined, undefined, { + before: [(context: TransformationContext) => new ReflectionTransformer(context).forHost(host.compilerHost).withReflectionMode('always')], }); - return result as any; + + return res as any; } /** @@ -1670,6 +1679,23 @@ test('InstanceType', () => { expect(type).toMatchObject({ kind: ReflectionKind.unknown }); }); +test('import types named import esm simple', () => { + const js = transpile({ + 'app': ` + import {User} from './user'; + typeOf(); + `, + 'user': `export interface User {id: number}` + }); + console.log('js', js); + expect(js['app.js']).toContain(`__ΩUser`); + + expect(js['user.js']).toContain(`export { __ΩUser as __ΩUser };`); + expect(js['user.d.ts']).toContain(`export declare type __ΩUser = any[]`); + + console.log(js); +}); + test('import types named import esm', () => { const js = transpile({ 'app': ` @@ -1681,6 +1707,7 @@ test('import types named import esm', () => { `, 'user': `export interface User {id: number}` }); + console.log('js', js); expect(js['app.js']).toContain(`__ΩUser`); expect(js['app.js']).toContain(`const __ΩPartial = [`); expect(js['app.js']).toContain(`export { __Ωbla as __Ωbla };`); @@ -1702,7 +1729,7 @@ test('import types named import cjs', () => { typeOf(); `, 'user': `export interface User {id: number}` - }, { ...options, module: ModuleKind.CommonJS }); + }, { module: ModuleKind.CommonJS }); console.log(js); expect(js['app.js']).toContain(`__ΩUser`); expect(js['app.js']).toContain(`const __ΩPartial = [`); @@ -1811,6 +1838,48 @@ test('enum literals', () => { // expect(type).toMatchObject({ kind: ReflectionKind.unknown }); }); +test('circular mapped type', () => { + //todo: fix this + const code = ` + + type Placeholder = () => T; + type Resolve }> = ReturnType; + type Replace = T & { _: Placeholder }; + + type Methods = { [K in keyof T]: K extends keyof Query ? never : K }; + + class Query { + //for higher kinded type for selected fields + _!: () => T; + + constructor( + classSchema: ReflectionClass, + protected session: DatabaseSession, + protected resolver: GenericQueryResolver + ) { + } + + public myMethod(): Methods> { + return undefined as any; + } + + protected async callOnFetchEvent(query: Query): Promise { + return undefined as any; + } + + public async find(): Promise[]> { + return undefined as any; + } + } + return typeOf>(); + `; + + const js = transpile(code); + console.log('js', js); + const type = transpileAndReturn(code); + console.log('type', type); +}); + test('pass type argument', () => { //not supported yet const code = ` @@ -1826,10 +1895,10 @@ test('pass type argument', () => { ` (globals.Targs = () => [__ΩUser], test)(); - ` + `; const t = () => a; - const a = '' + const a = ''; function test(a = (test as any).targs) { console.log('test', a); diff --git a/packages/type/tests/complex-query.spec.ts b/packages/type/tests/complex-query.spec.ts new file mode 100644 index 000000000..2e510222f --- /dev/null +++ b/packages/type/tests/complex-query.spec.ts @@ -0,0 +1,408 @@ +import { ReflectionClass, ReflectionProperty, typeOf } from '../src/reflection/reflection.js'; +import { expect, test } from '@jest/globals'; +import { isExtendable } from '../src/reflection/extends.js'; +import { stringifyResolvedType } from '../src/reflection/type.js'; + +export interface OrmEntity { +} + +export type FilterQuery = { + [P in keyof T & string]?: T[P]; +}; + +export type Placeholder = () => T; +export type Resolve }> = ReturnType; +export type Replace = T & { _: Placeholder }; + +export type FlattenIfArray = T extends Array ? T[0] : T; +export type FieldName = keyof T & string; + +export class DatabaseQueryModel { + public withIdentityMap: boolean = true; + public withChangeDetection: boolean = true; + public filter?: FILTER; + public having?: FILTER; + public groupBy: Set = new Set(); + public aggregate = new Map(); + public select: Set = new Set(); + public skip?: number; + public itemsPerPage: number = 50; + public limit?: number; + public parameters: { [name: string]: any } = {}; + public sort?: SORT; + public returning: (keyof T & string)[] = []; + + changed(): void { + } + + hasSort(): boolean { + return this.sort !== undefined; + } + + /** + * Whether limit/skip is activated. + */ + hasPaging(): boolean { + return this.limit !== undefined || this.skip !== undefined; + } + + setParameters(parameters: { [name: string]: any }) { + for (const [i, v] of Object.entries(parameters)) { + this.parameters[i] = v; + } + } + + clone(parentQuery: BaseQuery): this { + return this; + } + + /** + * Whether only a subset of fields are selected. + */ + isPartial() { + return this.select.size > 0 || this.groupBy.size > 0 || this.aggregate.size > 0; + } + + /** + * Whether only a subset of fields are selected. + */ + isAggregate() { + return this.groupBy.size > 0 || this.aggregate.size > 0; + } + + getFirstSelect() { + return this.select.values().next().value; + } + + isSelected(field: string): boolean { + return this.select.has(field); + } + + hasJoins() { + return false; + } + + hasParameters(): boolean { + return false; + } +} + +export class BaseQuery { + //for higher kinded type for selected fields + _!: () => T; + + protected createModel() { + return new DatabaseQueryModel(); + } + + public model: DatabaseQueryModel; + + constructor( + public readonly classSchema: ReflectionClass, + model?: DatabaseQueryModel + ) { + this.model = model || this.createModel(); + } + + groupBy[]>(...field: K): this { + const c = this.clone(); + c.model.groupBy = new Set([...field as string[]]); + return c as any; + } + + withSum, AS extends string>(field: K, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + return this.aggregateField(field, 'sum', as) as any; + } + + withGroupConcat, AS extends string>(field: K, as?: AS): Replace & { [C in [AS] as `${AS}`]: T[K][] }> { + return this.aggregateField(field, 'group_concat', as); + } + + withCount, AS extends string>(field: K, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + return this.aggregateField(field, 'count', as) as any; + } + + withMax, AS extends string>(field: K, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + return this.aggregateField(field, 'max', as) as any; + } + + withMin, AS extends string>(field: K, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + return this.aggregateField(field, 'min', as) as any; + } + + withAverage, AS extends string>(field: K, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + return this.aggregateField(field, 'avg', as) as any; + } + + aggregateField, AS extends string>(field: K, func: string, as?: AS): Replace & { [K in [AS] as `${AS}`]: number }> { + throw new Error(); + } + + select)[]>(...select: K): Replace, K[number]>> { + throw new Error(); + } + + returning(...fields: FieldName[]): this { + throw new Error(); + } + + skip(value?: number): this { + throw new Error(); + } + + /** + * Sets the page size when `page(x)` is used. + */ + itemsPerPage(value: number): this { + throw new Error(); + } + + /** + * Applies limit/skip operations correctly to basically have a paging functionality. + * Make sure to call itemsPerPage() before you call page. + */ + page(page: number): this { + throw new Error(); + } + + limit(value?: number): this { + throw new Error(); + } + + parameter(name: string, value: any): this { + throw new Error(); + } + + parameters(parameters: { [name: string]: any }): this { + throw new Error(); + } + + disableIdentityMap(): this { + throw new Error(); + } + + disableChangeDetection(): this { + throw new Error(); + } + + having(filter?: this['model']['filter']): this { + const c = this.clone(); + c.model.having = filter; + return c; + } + + filter(filter?: this['model']['filter']): this { + throw new Error(); + } + + addFilter(name: K, value: FilterQuery[K]): this { + throw new Error(); + } + + sort(sort?: this['model']['sort']): this { + throw new Error(); + } + + orderBy>(field: K, direction: 'asc' | 'desc' = 'asc'): this { + throw new Error(); + } + + clone(): this { + throw new Error(); + } + + /** + * Adds a left join in the filter. Does NOT populate the reference with values. + * Accessing `field` in the entity (if not optional field) results in an error. + */ + join>(field: K, type: 'left' | 'inner' = 'left', populate: boolean = false): this { + throw new Error(); + } +} + +export type Methods = { [K in keyof T]: K extends keyof Query ? never : T[K] extends ((...args: any[]) => any) ? K : never }[keyof T]; + +/** + * This a generic query abstraction which should supports most basics database interactions. + * + * All query implementations should extend this since db agnostic consumers are probably + * coded against this interface via Database which uses this GenericQuery. + */ +export class Query { + // protected lifts: ClassType[] = []; + // + // static is>>(v: Query, type: T): v is InstanceType { + // return v.lifts.includes(type) || v instanceof type; + // } + // + // constructor( + // classSchema: ReflectionClass, + // // protected session: DatabaseSession, + // // protected resolver: GenericQueryResolver + // ) { + // super(classSchema); + // } + // + // static from & { _: () => T }, T extends ReturnType['_']>, B extends ClassType>>(this: B, query: Q): Replace, Resolve> { + // throw new Error(); + // } + // + // public lift>, T extends ReturnType['_']>, THIS extends Query & { _: () => T }>( + // this: THIS, query: B + // ): Replace, Resolve> & Pick> { + // throw new Error(); + // } + // + // clone(): this { + // throw new Error(); + // } + // + // protected async callOnFetchEvent(query: Query): Promise { + // throw new Error(); + // } + // + // protected onQueryResolve(query: Query): this { + // throw new Error(); + // } + // + // public async count(fromHas: boolean = false): Promise { + // throw new Error(); + // } + // + // public async find(): Promise[]> { + // throw new Error(); + // } + // + // public async findOneOrUndefined(): Promise { + // throw new Error(); + // } + // + // public async findOne(): Promise> { + // throw new Error(); + // } + // // // + // // // public async deleteMany(): Promise> { + // // // throw new Error(); + // // // } + // // // + // // // public async deleteOne(): Promise> { + // // // throw new Error(); + // // // } + // // // + // // // protected async delete(query: Query): Promise> { + // // // throw new Error(); + // // // } + // // + // // // public async patchMany(patch: ChangesInterface | Partial): Promise> { + // // // throw new Error(); + // // // } + // // // + // // // public async patchOne(patch: ChangesInterface | Partial): Promise> { + // // // throw new Error(); + // // // } + // // // + // // // protected async patch(query: Query, patch: Partial | ChangesInterface): Promise> { + // // // throw new Error(); + // // // } + // // // + // // // public async has(): Promise { + // // // throw new Error(); + // // // } + // // // + // // // public async ids(singleKey?: false): Promise[]>; + // // // public async ids(singleKey: true): Promise[]>; + // // // public async ids(singleKey: boolean = false): Promise[] | PrimaryKeyType[]> { + // // // throw new Error(); + // // // } + + public findField>(name: K): FieldName { + throw new Error(); + } + + // public async findOneField>(name: K): Promise { + // throw new Error(); + // } + // + // public async findOneFieldOrUndefined>(name: K): Promise { + // throw new Error(); + // } +} + +interface User { + id: number; + username: string; +} + +test('complex query', () => { + + const queryUser = typeOf>(); + const queryAny = typeOf>(); + + type t1 = Query extends Query ? true : never; + type t2 = Query extends Query ? true : never; + + + type t3 = FieldName; + expect(stringifyResolvedType(typeOf())).toBe('string'); + type t4 = FieldName; + + type t5 = FieldName extends FieldName ? true : never; + type t6 = FieldName extends FieldName ? true : never; + + expect(isExtendable(typeOf>(), typeOf>())).toBe(true); + expect(isExtendable(typeOf>(), typeOf>())).toBe(false); + + expect(isExtendable(queryUser, queryAny)).toBe(true); + expect(isExtendable(queryAny, queryUser)).toBe(false); +}); + +test('T in constraint', () => { + type Q = {a: keyof T & string}; + + type qAny = Q; + type qUser = Q; + + type t0 = any extends User ? true : never; + type t1 = Q extends Q ? true : never; + type t2 = qAny extends qUser ? true : never; + type t3 = {a: string} extends {a: 'id' | 'username'} ? true : never; + type t4 = string extends 'id' | 'username' ? true : never; + + interface Q1 { + a: T & string; + } + + interface Q2 { + findField(): string; + } + + interface Q3 { + findField(): 'id' | 'username'; + } + // type t11 = Q1 extends Q1<'id' | 'username'> ? true : never; + // type t2 = Q extends Q ? true : never; + // type t21 = Q1<'id' | 'username'> extends Q1 ? true : never; + // type t22 = Q2 extends Q3 ? true : never; + // + // type b1 = ReturnType['findField']>; + // type b2 = ReturnType['findField']>; + // type t3 = ReturnType['findField']> extends ReturnType['findField']> ? true : never; + // type t31 = string extends 'user' ? true : never; + // type t32 = { a: string } extends { a: 'user' } ? true : never; + // type t33 = { a(): string } extends { a(): 'user' } ? true : never; + // + // const qAny = typeOf>(); + // const qUser = typeOf>(); + // + // const c1 = ReflectionClass.from(qAny); + // const c2 = ReflectionClass.from(qUser); + // + // console.log(stringifyResolvedType(c1.getMethod('findField').getReturnType())); + // console.log(stringifyResolvedType(c2.getMethod('findField').getReturnType())); + // + // // console.log(stringifyResolvedType(c1.type)); + // // console.log(stringifyResolvedType(c2.type)); + // + // expect(isExtendable(qUser, qAny)).toBe(true); + // expect(isExtendable(qAny, qUser)).toBe(true); +}); diff --git a/packages/type/tests/integration.spec.ts b/packages/type/tests/integration.spec.ts index 157ba87f0..efdc1cc67 100644 --- a/packages/type/tests/integration.spec.ts +++ b/packages/type/tests/integration.spec.ts @@ -11,7 +11,7 @@ import { ClassType } from '@deepkit/core'; import { expect, test } from '@jest/globals'; import { entity, t } from '../src/decorator'; -import { propertiesOf, reflect, ReflectionClass, ReflectionFunction, typeOf, valuesOf } from '../src/reflection/reflection'; +import { propertiesOf, reflect, ReflectionClass, ReflectionFunction, ReflectionMethod, typeOf, valuesOf } from '../src/reflection/reflection'; import { annotateClass, assertType, @@ -52,7 +52,7 @@ import { Unique } from '../src/reflection/type'; import { TypeNumberBrand } from '@deepkit/type-spec'; -import { validate, ValidatorError } from '../src/validator'; +import { MinLength, validate, ValidatorError } from '../src/validator'; import { expectEqualType } from './utils'; import { MyAlias } from './types'; import { resolveRuntimeType } from '../src/reflection/processor'; @@ -171,6 +171,42 @@ test('class constructor', () => { } as Type); }); +test('class extends another', () => { + class Class1 { + constructor(title: string) { + } + } + + class Class2 extends Class1 { + constructor() { + super('asd'); + } + } + + const reflection = ReflectionClass.from(Class2); + const constructor = reflection.getMethodOrUndefined('constructor'); + expect(constructor).toBeInstanceOf(ReflectionMethod); + expect(constructor!.getParameters().length).toBe(0); +}); + +test('class expression extends another', () => { + class Class1 { + constructor(title: string) { + } + } + + const class2 = class extends Class1 { + constructor() { + super('asd'); + } + } + + const reflection = ReflectionClass.from(class2); + const constructor = reflection.getMethodOrUndefined('constructor'); + expect(constructor).toBeInstanceOf(ReflectionMethod); + expect(constructor!.getParameters().length).toBe(0); +}); + test('constructor type abstract', () => { type constructor = abstract new (...args: any) => any; expectEqualType(typeOf(), { @@ -927,7 +963,7 @@ test('reflection function', () => { reflection.getReturnType(); //[void] expect(reflection.getParameterNames()).toEqual(['text']); - expect(reflection.getParameter('text')!.kind).toBe(ReflectionKind.parameter); + expect(reflection.getParameter('text')!.getType().kind).toBe(ReflectionKind.string); expect(reflection.getParameterType('text')!.kind).toBe(ReflectionKind.string); expect(reflection.getReturnType().kind).toBe(ReflectionKind.void); @@ -1527,44 +1563,44 @@ test('set constructor parameter manually', () => { } } - { - const reflection = reflect(StreamApiResponseClass); - assertType(reflection, ReflectionKind.class); - - // type T = StreamApiResponseClass; - // type a = T['response']; - //if there is no type passed to T it resolved to any - expect(reflection.typeArguments).toEqual([{ kind: ReflectionKind.any }]); - } - - { - class StreamApiResponseClassWithDefault { - constructor(public response: T) { - } - } - - const reflection = reflect(StreamApiResponseClassWithDefault); - assertType(reflection, ReflectionKind.class); - - // type T = StreamApiResponseClassWithDefault; - // type a = T['response']; - expect(reflection.typeArguments).toMatchObject([{ kind: ReflectionKind.string }]); - } - - expectEqualType(typeOf(), { - kind: ReflectionKind.class, - classType: Response, - types: [ - { - kind: ReflectionKind.method, name: 'constructor', visibility: ReflectionVisibility.public, parameters: [ - { kind: ReflectionKind.parameter, name: 'success', visibility: ReflectionVisibility.public, type: { kind: ReflectionKind.boolean } } - ], return: { kind: ReflectionKind.any } - }, - { - kind: ReflectionKind.property, visibility: ReflectionVisibility.public, name: 'success', type: { kind: ReflectionKind.boolean } - } - ] - } as Type); + // { + // const reflection = reflect(StreamApiResponseClass); + // assertType(reflection, ReflectionKind.class); + // + // // type T = StreamApiResponseClass; + // // type a = T['response']; + // //if there is no type passed to T it resolved to any + // expect(reflection.typeArguments).toEqual([{ kind: ReflectionKind.any }]); + // } + // + // { + // class StreamApiResponseClassWithDefault { + // constructor(public response: T) { + // } + // } + // + // const reflection = reflect(StreamApiResponseClassWithDefault); + // assertType(reflection, ReflectionKind.class); + // + // // type T = StreamApiResponseClassWithDefault; + // // type a = T['response']; + // expect(reflection.typeArguments).toMatchObject([{ kind: ReflectionKind.string }]); + // } + // + // expectEqualType(typeOf(), { + // kind: ReflectionKind.class, + // classType: Response, + // types: [ + // { + // kind: ReflectionKind.method, name: 'constructor', visibility: ReflectionVisibility.public, parameters: [ + // { kind: ReflectionKind.parameter, name: 'success', visibility: ReflectionVisibility.public, type: { kind: ReflectionKind.boolean } } + // ], return: { kind: ReflectionKind.any } + // }, + // { + // kind: ReflectionKind.property, visibility: ReflectionVisibility.public, name: 'success', type: { kind: ReflectionKind.boolean } + // } + // ] + // } as Type); function StreamApiResponse(responseBodyClass: ClassType) { class A extends StreamApiResponseClass { @@ -1582,6 +1618,7 @@ test('set constructor parameter manually', () => { expect(reflection.getMethods().length).toBe(1); expect(reflection.getProperties().length).toBe(1); expect(reflection.getMethod('constructor')!.getParameters().length).toBe(1); + //if this fails, ClassType can probably not be resolved expect(reflection.getMethod('constructor')!.getParameter('response')!.getType().kind).toBe(ReflectionKind.class); expect(reflection.getMethods()[0].getName()).toBe('constructor'); const responseType = reflection.getProperty('response')!.getType(); @@ -1979,7 +2016,7 @@ test('test', () => { title?: string; } - validate
({id: 1}).length; //0, means it validated successfully + validate
({ id: 1 }).length; //0, means it validated successfully validate
({}).length; //1, means there are validation errors console.log(validate
({})); diff --git a/packages/type/tests/simple-decorator.spec.ts b/packages/type/tests/simple-decorator.spec.ts new file mode 100644 index 000000000..bb640c2d5 --- /dev/null +++ b/packages/type/tests/simple-decorator.spec.ts @@ -0,0 +1,19 @@ +/* + * Deepkit Framework + * Copyright Deepkit UG, Marc J. Schmidt + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the MIT License. + * + * You should have received a copy of the MIT License along with this program. + */ + +import { expect, test } from '@jest/globals'; +import { typeOf } from '../src/reflection/reflection'; +import { PrimaryKey, primaryKeyAnnotation } from '../src/reflection/type'; + +test('primary key', () => { + type t = number & PrimaryKey; + const type = typeOf(); + expect(primaryKeyAnnotation.isPrimaryKey(type)).toBe(true); +}); diff --git a/packages/type/tests/type.spec.ts b/packages/type/tests/type.spec.ts index c88ada9f5..0b8cacc0e 100644 --- a/packages/type/tests/type.spec.ts +++ b/packages/type/tests/type.spec.ts @@ -3,23 +3,28 @@ import { hasCircularReference, ReceiveType, reflect, ReflectionClass, resolveRec import { assertType, Embedded, + Excluded, excludedAnnotation, findMember, + Group, groupAnnotation, indexAccess, InlineRuntimeType, isSameType, + metaAnnotation, ReflectionKind, + ResetDecorator, stringifyResolvedType, stringifyType, Type, TypeClass, TypeObjectLiteral, TypeProperty, - UUID + UUID, validationAnnotation } from '../src/reflection/type'; import { isExtendable } from '../src/reflection/extends'; import { expectEqualType } from './utils'; import { ClassType } from '@deepkit/core'; import { Partial } from '../src/changes'; +import { MaxLength, MinLength } from '../src/validator'; //note: this needs to run in a strict TS mode to infer correctly in the IDE type Extends = [A] extends [B] ? true : false; @@ -45,6 +50,72 @@ test('stringify date/set/map', () => { expect(stringifyType(typeOf>())).toBe('Set'); }); +test('type decorator', () => { + type MyAnnotation = { __meta?: ['myAnnotation'] }; + type Username = string & MyAnnotation; + const type = typeOf(); + const data = metaAnnotation.getForName(type, 'myAnnotation'); + expect(data).toEqual([]); +}); + +test('copy index access', () => { + interface User { + password: string & MinLength<6> & MaxLength<30>; + } + + interface UserCreationPayload { + password: User['password'] & Group<'a'>; + } + + const type = typeOf(); + + assertType(type, ReflectionKind.objectLiteral); + const password = findMember('password', type); + assertType(password, ReflectionKind.propertySignature); + assertType(password.type, ReflectionKind.string); + const validations = validationAnnotation.getAnnotations(password.type); + expect(validations[0].name).toBe('minLength'); + expect(validations[1].name).toBe('maxLength'); + const groups = groupAnnotation.getAnnotations(password.type); + expect(groups[0]).toBe('a'); +}); + +test('reset type decorator', () => { + interface User { + password: string & MinLength<6> & Excluded<'json'>; + } + + interface UserCreationPayload { + password: User['password'] & Group<'a'> & ResetDecorator<'excluded'>; + } + + { + const type = typeOf(); + assertType(type, ReflectionKind.objectLiteral); + const password = findMember('password', type); + assertType(password, ReflectionKind.propertySignature); + assertType(password.type, ReflectionKind.string); + const validations = validationAnnotation.getAnnotations(password.type); + expect(validations[0].name).toBe('minLength'); + const groups = groupAnnotation.getAnnotations(password.type); + expect(groups[0]).toBe('a'); + expect(excludedAnnotation.isExcluded(password.type, 'json')).toBe(false); + } + + { + const type = typeOf(); + assertType(type, ReflectionKind.objectLiteral); + const password = findMember('password', type); + assertType(password, ReflectionKind.propertySignature); + assertType(password.type, ReflectionKind.string); + const validations = validationAnnotation.getAnnotations(password.type); + expect(validations[0].name).toBe('minLength'); + const groups = groupAnnotation.getAnnotations(password.type); + expect(groups).toEqual([]); + expect(excludedAnnotation.isExcluded(password.type, 'json')).toBe(true); + } +}) + test('type alias preserved', () => { type MyString = string; expect(stringifyType(typeOf())).toBe('MyString'); @@ -133,6 +204,16 @@ test('extendability union', () => { invalidExtend(); }); +test('promise', () => { + validExtend, Promise>(); + validExtend, Promise>(); + invalidExtend, Promise>(); + invalidExtend, never>(); + validExtend>(); + validExtend>(); + validExtend, any>(); +}); + test('interface with method', () => { interface Connection { id: number; diff --git a/packages/type/tests/validation.spec.ts b/packages/type/tests/validation.spec.ts index c091a984b..3ce76fbd0 100644 --- a/packages/type/tests/validation.spec.ts +++ b/packages/type/tests/validation.spec.ts @@ -5,6 +5,14 @@ import { AutoIncrement, Excluded, Group, integer, PrimaryKey, Type, Unique } fro import { t } from '../src/decorator'; import { ReflectionClass, typeOf } from '../src/reflection/reflection'; +test('primitives', () => { + expect(validate('Hello')).toEqual([]); + expect(validate(123)).toEqual([{code: 'type', message: 'Not a string', path: ''}]); + + expect(validate('Hello')).toEqual([{code: 'type', message: 'Not a number', path: ''}]); + expect(validate(123)).toEqual([]); +}); + test('email', () => { expect(is('peter@example.com')).toBe(true); expect(is('nope')).toBe(false); @@ -80,6 +88,18 @@ test('decorator validator', () => { expect(validate({ username: 'Pe' })).toEqual([{ path: 'username', code: 'length', message: `Min length of 3` }]); }); +test('simple interface', () => { + interface User { + id: number; + username: string; + } + + expect(validate(undefined)).toEqual([{code: 'type', message: 'Not an object', path: ''}]); + expect(validate({})).toEqual([{code: 'type', message: 'Not a number', path: 'id'}]) + expect(validate({id: 1})).toEqual([{code: 'type', message: 'Not a string', path: 'username'}]) + expect(validate({id: 1, username: 'Peter'})).toEqual([]) +}); + test('class', () => { class User { id: integer & PrimaryKey & AutoIncrement & Positive = 0; @@ -153,13 +173,3 @@ test('inherited validations', () => { expect(validate({ username: 'Pe' })).toEqual([{code: 'minLength', message: 'Min length is 3', path: 'username'}]); expect(validate({ username: 'Peter' })).toEqual([]); }); - -test('asdasd', () => { - interface User { - id: number; - username: string; - } - - const errors = validate({}); - console.log('errors', errors); -}); diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index 41bf8b2b7..1b99d910a 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -10,7 +10,7 @@ import { capitalize, ClassType, CompilerContext, CustomError, ExtractClassType, getClassName, isArray, toFastProperties } from '@deepkit/core'; import { BaseEvent, EventDispatcher, EventToken, isEventListenerContainerEntryCallback, isEventListenerContainerEntryService } from '@deepkit/event'; -import { InjectorContext } from '@deepkit/injector'; +import { injectedFunction, InjectorContext } from '@deepkit/injector'; import { FrameCategory, Stopwatch } from '@deepkit/stopwatch'; interface WorkflowTransition { @@ -96,7 +96,7 @@ export class WorkflowDefinition { } getEventToken(name: K): EventToken> { - if (!this.tokens[name]) throw new Error(`No event token found for ${name}`); + if (!this.tokens[name]) throw new Error(`No event token found for ${String(name)}`); return this.tokens[name]!; } @@ -107,8 +107,8 @@ export class WorkflowDefinition { this.next[from]!.push(to); } - public create(state: keyof T & string, eventDispatcher: EventDispatcher, injectorContext?: InjectorContext, stopwatch?: Stopwatch): Workflow { - return new Workflow(this, new WorkflowStateSubject(state), eventDispatcher, injectorContext || eventDispatcher.scopedContext, stopwatch); + public create(state: keyof T & string, eventDispatcher: EventDispatcher, injector?: InjectorContext, stopwatch?: Stopwatch): Workflow { + return new Workflow(this, new WorkflowStateSubject(state), eventDispatcher, injector || eventDispatcher.injector, stopwatch); } getTransitionsFrom(state: keyof T & string): (keyof T & string)[] { @@ -141,12 +141,18 @@ export class WorkflowDefinition { const listenerCode: string[] = []; for (const listener of listeners) { if (isEventListenerContainerEntryCallback(listener)) { - const fnVar = compiler.reserveVariable('fn', listener.fn); - listenerCode.push(` + try { + const injector = listener.module ? eventDispatcher.injector.getInjector(listener.module) : eventDispatcher.injector.getRootInjector(); + const fn = injectedFunction(listener.fn, injector, 1); + const fnVar = compiler.reserveVariable('fn', fn); + listenerCode.push(` if (!event.isStopped()) { - await ${fnVar}(event); + await ${fnVar}(scopedContext.scope, event); } `); + } catch (error: any) { + throw new Error(`Could not build workflow listener ${listener.fn.name || 'anonymous function'} of event token ${eventToken.id}: ${error.message}`); + } } else if (isEventListenerContainerEntryService(listener)) { const classTypeVar = compiler.reserveVariable('classType', listener.classType); const moduleVar = listener.module ? ', ' + compiler.reserveVariable('module', listener.module) : ''; @@ -238,7 +244,7 @@ export class Workflow { public definition: WorkflowDefinition, public state: WorkflowState, private eventDispatcher: EventDispatcher, - private injectorContext: InjectorContext, + private injector: InjectorContext, private stopwatch?: Stopwatch ) { } @@ -259,7 +265,7 @@ export class Workflow { fn = (this.eventDispatcher as any)[this.definition.symbol] = this.definition.buildApplier(this.eventDispatcher); } - return fn(this.injectorContext, this.state, nextState, event || new WorkflowEvent() as ExtractClassType, this.stopwatch); + return fn(this.injector, this.state, nextState, event || new WorkflowEvent() as ExtractClassType, this.stopwatch); } isDone(): boolean { diff --git a/packages/workflow/tests/workflow.spec.ts b/packages/workflow/tests/workflow.spec.ts index c7d87d0dc..b1602a5ae 100644 --- a/packages/workflow/tests/workflow.spec.ts +++ b/packages/workflow/tests/workflow.spec.ts @@ -57,7 +57,7 @@ test('workflow events', async () => { const w = workflow1.create('start', dispatcher); let called = false; - dispatcher.registerCallback(workflow1.onDoIt, async () => { + dispatcher.listen(workflow1.onDoIt, async () => { called = true; }); @@ -94,15 +94,15 @@ test('workflow events apply next', async () => { const w = workflow1.create('start', dispatcher); let endCalled = false; - dispatcher.registerCallback(workflow1.onDoIt, async (event) => { + dispatcher.listen(workflow1.onDoIt, async (event) => { event.next('success'); }); - dispatcher.registerCallback(workflow1.onSuccess, async (event) => { + dispatcher.listen(workflow1.onSuccess, async (event) => { event.next('end', new EndEvent()); }); - dispatcher.registerCallback(workflow1.onEnd, async (event) => { + dispatcher.listen(workflow1.onEnd, async (event) => { expect(event.test).toBe('hi'); endCalled = true; }); @@ -117,7 +117,7 @@ test('workflow events apply next invalid', async () => { const dispatcher = new EventDispatcher(InjectorContext.forProviders([])); const w = workflow1.create('start', dispatcher); - dispatcher.registerCallback(workflow1.onDoIt, async (event) => { + dispatcher.listen(workflow1.onDoIt, async (event) => { event.next('end'); });